Weiter zum Inhalt
  • Home
  • Aktuell
  • Tags
  • 0 Ungelesen 0
  • Kategorien
  • Unreplied
  • Beliebt
  • GitHub
  • Docu
  • Hilfe
Skins
  • Hell
  • Brite
  • Cerulean
  • Cosmo
  • Flatly
  • Journal
  • Litera
  • Lumen
  • Lux
  • Materia
  • Minty
  • Morph
  • Pulse
  • Sandstone
  • Simplex
  • Sketchy
  • Spacelab
  • United
  • Yeti
  • Zephyr
  • Dunkel
  • Cyborg
  • Darkly
  • Quartz
  • Slate
  • Solar
  • Superhero
  • Vapor

  • Standard: (Kein Skin)
  • Kein Skin
Einklappen
ioBroker Logo

Community Forum

donate donate
  1. ioBroker Community Home
  2. Deutsch
  3. Skripten / Logik
  4. Dynamische SVG-Progress-Bar

NEWS

  • wichtiges UPDATE für controller 7.2.2 im stable
    HomoranH
    Homoran
    10
    1
    2.2k

  • Neues YouTube-Video: Visualisierung im Devices-Adapter
    BluefoxB
    Bluefox
    16
    1
    4.1k

  • Neuer ioBroker-Blog online: Monatsrückblick März/April 2026
    BluefoxB
    Bluefox
    8
    1
    3.6k

Dynamische SVG-Progress-Bar

Geplant Angeheftet Gesperrt Verschoben Skripten / Logik
javascriptmultimediamonitoring
6 Beiträge 2 Kommentatoren 726 Aufrufe 4 Beobachtet
  • Älteste zuerst
  • Neuste zuerst
  • Meiste Stimmen
Antworten
  • In einem neuen Thema antworten
Anmelden zum Antworten
Dieses Thema wurde gelöscht. Nur Nutzer mit entsprechenden Rechten können es sehen.
  • Ro75R Online
    Ro75R Online
    Ro75
    schrieb am zuletzt editiert von Ro75
    #1

    Dieses Skript bietet eine elegante Lösung, um ohne aufwendige Widget-Konfigurationen voll anpassbare Fortschrittsbalken (horizontal oder vertikal) direkt per JavaScript zu generieren. Ideal für Dashboards, bei denen Füllstände, Batteriestände oder Sensorwerte grafisch ansprechend visualisiert werden sollen.

    c39bcee3-eb86-4bf4-9cb5-cb2138ebdfe8-image.jpeg

    Hinweis: Dieses Skript wurde mit Unterstützung von KI entwickelt, um eine effiziente und saubere Code-Struktur für die dynamische SVG-Generierung sicherzustellen.

    Warum dieses Skript?
    In der Visualisierung (VIS) stößt man bei vielen gleichartigen Elementen oft an Grenzen. Anstatt für jedes Widget manuell Einstellungen vorzunehmen, generiert dieses Skript den SVG-Code "on-the-fly".

    Einheitliches Design:
    Alle Balken sind konsistent.

    Flexibel:
    Einfache Anpassung von Farben, Größen und Stilen über Parameter.

    Was ist nötig?
    Du benötigst die Instanz javascript.

    Datenpunkt:
    Erstelle einen Datenpunkt (Typ String) in 0_userdata.0, in dem das generierte SVG gespeichert wird.

    Widget:
    Erstelle in deiner VIS ein String (unescaped) Widget und binde den Datenpunkt über die Objekt ID ein: {0_userdata.0.dein.datenpunkt}.

    Die Parameter-Erklärung

    Parameter Typ Beschreibung
    orientation String "horizontal" oder "vertical"
    value Number Aktueller Wert in Prozent (0 - 100)
    progressColor String Farbe des Balkens (Hex, RGB oder RGBA)
    backgroundColor String Hintergrundfarbe (Hex, RGB, RGBA, "transparent")
    border Number Rahmenstärke in Pixeln
    borderColor String Rahmenfarbe (Hex, RGB oder RGBA)
    borderRadius Number Eckenabrundung in Pixeln
    labelFont String Schriftart (z.B. "Arial")
    labelSize String Schriftgröße (z.B. "20px") "0px" wenn das Label ausgeblendet werden soll
    labelColor String Textfarbe (Hex, RGB oder RGBA)
    labelshadow Boolean true für dezenten Text-Schatten
    labelsuffix String Anhang hinter dem Wert (z.B. "%")
    gradient Boolean true für automatischen Farbverlauf
    effect String "glanz" für Glas-Effekt oder "none"
    maxValue Number Standardwert = 100, kann aber auch jeder andere Wert sein
    width Number Breite, für Automatik null belassen, ansonsten gewünschten Wert - z.B. 200
    height Number Höhe, für Automatik null belassen, ansonsten gewünschten Wert - z.B. 40

    Farbangaben & Transparenz
    Die Funktion ist für Standard-Farben ausgelegt. Du kannst für alle Farb-Parameter (progressColor, backgroundColor, borderColor, labelColor) die folgenden Formate verwenden:

    Hex-Werte: (z. B. "#2a2a2a"). Wichtig: Für den Parameter gradient = true solltest du bevorzugt Hex-Werte verwenden, da die interne Logik für Farbverläufe darauf optimiert ist.

    RGB / RGBA: Du kannst Farben als rgb(r, g, b) oder mit Transparenz als rgba(r, g, b, alpha) definieren.

    Beispiel: "rgba(0, 128, 0, 0.5)" für ein halbtransparentes Grün.

    "transparent": Verwende das Schlüsselwort "transparent", um den Hintergrund komplett auszublenden.

    Hier das Script:

    // === Dynamische SVG-Progress-Bar ===
     
    //Erst-Version 1.0.0 - 01.07.2026
    //Version 1.0.2 - 02.07.2026
    //Ersteller Ro75.
     
    //Voraussetzungen (Version 1.0.2 getestet mit)
    //NodeJS: 22.22.x
    //Javascript-Adapter: 9.0.18
    //Admin-Adapter: 7.8.23
    //js-controler: 7.2.2
    
    function buildProgressBarSvg(orientation, value, progressColor, backgroundColor, border, borderColor, borderRadius, labelFont, labelSize, labelColor, labelshadow, labelsuffix, gradient, effect, maxValue = 100, Width = null, Height = null) {
        const pct = (maxValue === 0) ? 0 : (value / maxValue) * 100;
    
        let svgW, svgH;
        if (orientation === "horizontal") {
            svgW = 80;
            svgH = 15;
        } else {
            svgW = 15;
            svgH = 80;
        }
    
        if (Width !== null) svgW = Width;
        if (Height !== null) svgH = Height;
    
        function adjustColor(color, lightnessChange) {
            let r, g, b, a = null;
    
            if (color.startsWith("#")) {
                let hex = color.slice(1);
                if (hex.length === 3) hex = hex.split("").map(c => c + c).join("");
                const num = parseInt(hex, 16);
                r = (num >> 16) & 255;
                g = (num >> 8) & 255;
                b = num & 255;
            } else if (color.startsWith("rgb")) {
                const parts = color.match(/[\d.]+/g).map(Number);
                [r, g, b] = parts;
                if (parts.length === 4) a = parts[3];
            }
            else return color;
    
            r /= 255; g /= 255; b /= 255;
    
            const max = Math.max(r, g, b);
            const min = Math.min(r, g, b);
            let h, s;
            let l = (max + min) / 2;
    
            if (max === min) {
                h = s = 0;
            } else {
                const d = max - min;
                s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
    
                switch (max) {
                    case r: h = (g - b) / d + (g < b ? 6 : 0); break;
                    case g: h = (b - r) / d + 2; break;
                    case b: h = (r - g) / d + 4; break;
                }
                h /= 6;
            }
    
            l += lightnessChange / 100;
            l = Math.max(0, Math.min(1, l));
    
            function hue2rgb(p, q, t) {
                if (t < 0) t += 1;
                if (t > 1) t -= 1;
                if (t < 1/6) return p + (q - p) * 6 * t;
                if (t < 1/2) return q;
                if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
                return p;
            }
    
            let rr, gg, bb;
    
            if (s === 0) {
                rr = gg = bb = l;
            } else {
                const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
                const p = 2 * l - q;
    
                rr = hue2rgb(p, q, h + 1/3);
                gg = hue2rgb(p, q, h);
                bb = hue2rgb(p, q, h - 1/3);
            }
    
            rr = Math.round(rr * 255);
            gg = Math.round(gg * 255);
            bb = Math.round(bb * 255);
    
            if (a !== null) return `rgba(${rr},${gg},${bb},${a})`;
            return "#" + ((1 << 24) | (rr << 16) | (gg << 8) | bb).toString(16).slice(1);
        }
    
        let finalProgressColor = progressColor;
        let gradientDef = "";
    
        if (gradient === true) {
            const gradId = "grad_" + Math.random().toString(36).slice(2);
    
            const c1 = adjustColor(progressColor, -25);
            const c2 = adjustColor(progressColor, -15);
            const c3 = adjustColor(progressColor, -5);
            const c4 = adjustColor(progressColor, 5);
            const c5 = adjustColor(progressColor, 15);
            const c6 = adjustColor(progressColor, 25);
    
            if (orientation === "horizontal") {
                gradientDef = `
                    <linearGradient id="${gradId}" x1="0" y1="0" x2="1" y2="0">
                        <stop offset="0%"   stop-color="${c1}"/>
                        <stop offset="20%"  stop-color="${c2}"/>
                        <stop offset="40%"  stop-color="${c3}"/>
                        <stop offset="60%"  stop-color="${c4}"/>
                        <stop offset="80%"  stop-color="${c5}"/>
                        <stop offset="100%" stop-color="${c6}"/>
                    </linearGradient>
                `;
            } else {
                gradientDef = `
                    <linearGradient id="${gradId}" x1="0" y1="1" x2="0" y2="0">
                        <stop offset="0%"   stop-color="${c1}"/>
                        <stop offset="20%"  stop-color="${c2}"/>
                        <stop offset="40%"  stop-color="${c3}"/>
                        <stop offset="60%"  stop-color="${c4}"/>
                        <stop offset="80%"  stop-color="${c5}"/>
                        <stop offset="100%" stop-color="${c6}"/>
                    </linearGradient>
                `;
            }
            finalProgressColor = `url(#${gradId})`;
        }
    
        const innerX = border;
        const innerY = border;
        const innerW = svgW - border * 2;
        const innerH = svgH - border * 2;
    
        const outerRadius = borderRadius;
        const innerRadius = Math.max(0, outerRadius - border);
        const effectiveRadius = innerRadius - 0.3;
    
        const clipId = "clip_" + Math.random().toString(36).slice(2);
        const glossClipId = "clip_gloss_" + Math.random().toString(36).slice(2);
    
        let clipDef = "";
        let glossClipDef = "";
    
        if (pct > 0) {
            if (orientation === "horizontal") {
                const fillW = innerW * (pct / 100);
    
                clipDef = `<clipPath id="${clipId}"><rect x="${innerX}" y="${innerY}" width="${fillW}" height="${innerH}" /></clipPath>`;
    
                if (effect === "glanz") {
                    const glossH = innerH * 0.45;
                    glossClipDef = `<clipPath id="${glossClipId}"><rect x="${innerX}" y="${innerY}" width="${fillW}" height="${glossH}" /></clipPath>`;
                }
            } else {
                const fillH = innerH * (pct / 100);
                const yFill = innerY + innerH - fillH;
    
                clipDef = `<clipPath id="${clipId}"><rect x="${innerX}" y="${yFill}" width="${innerW}" height="${fillH}" /></clipPath>`;
    
                if (effect === "glanz") {
                    const glossW = innerW * 0.45;
                    glossClipDef = `<clipPath id="${glossClipId}"><rect x="${innerX}" y="${yFill}" width="${glossW}" height="${fillH}" /></clipPath>`;
                }
            }
        }
    
        const labelText = Math.round(pct * 100) / 100 + " " + labelsuffix;
        const shadow = labelshadow ? "text-shadow:1px 1px 2px black;" : "";
    
        return `
            <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 ${svgW} ${svgH}" preserveAspectRatio="none">
                <defs>
                    ${gradientDef}
                    ${clipDef}
                    ${glossClipDef}
                </defs>
    
                <rect x="0" y="0" width="${svgW}" height="${svgH}" fill="${borderColor}" rx="${outerRadius}" ry="${outerRadius}" />
                <rect x="${innerX}" y="${innerY}" width="${innerW}" height="${innerH}" fill="${backgroundColor}" rx="${innerRadius}" ry="${innerRadius}" />
    
                ${pct > 0 ? `<rect x="${innerX}" y="${innerY}" width="${innerW}" height="${innerH}" fill="${finalProgressColor}" rx="${effectiveRadius}" ry="${effectiveRadius}" clip-path="url(#${clipId})" />` : ""}
                ${effect === "glanz" && pct > 0 ? `<rect x="${innerX}" y="${innerY}" width="${innerW}" height="${innerH}" fill="rgba(255,255,255,0.35)" rx="${effectiveRadius}" ry="${effectiveRadius}" clip-path="url(#${glossClipId})" />` : ""}
    
                <foreignObject x="0" y="0" width="${svgW}" height="${svgH}">
                    <div xmlns="http://www.w3.org/1999/xhtml"
                        style="
                            position:absolute;
                            top:50%;
                            left:50%;
                            transform:translate(-50%,-50%) ${orientation === 'vertical' ? 'rotate(-90deg)' : ''};
                            font-family:${labelFont};
                            font-size:${labelSize};
                            font-weight:bold;
                            color:${labelColor};
                            ${shadow}
                            white-space:nowrap;
                            pointer-events:none;
                        ">
                        ${labelText}
                    </div>
                </foreignObject>
            </svg>
        `;
    }
    

    Anwendung mit on()-Trigger (Automatische Aktualisierung):
    Anstatt den Wert manuell zu setzen, solltest du das Skript durch einen Trigger auslösen lassen. So aktualisiert sich deine ProgressBar automatisch, sobald sich der Wert deines Sensors ändert:

    // Beispiel: Überwache einen Datenpunkt (z.B. einen Füllstand)
    on({ id: 'alias.0.Geräte.Level', change: 'any' }, function (obj) {
        
        // Die ProgressBar wird bei jeder Änderung des Datenpunktes neu berechnet
        let barSVG = buildProgressBarSvg(
            "horizontal", 
            obj.state.val,              // Der aktuelle Wert vom Sensor
            "#004400",                  // progressColor
            "#333333",                  // backgroundColor
            0.5,                        // border
            "white",                    // borderColor
            5,                          // borderRadius
            "Arial",                    // labelFont
            "6px",                      // labelSize
            "#ffffff",                  // labelColor
            true,                       // labelshadow
            "%",                        // labelsuffix
            true,                       // gradient
            "none",                     // effect
            100                         // maxValue
            200                         // width
            40                          // height
        );
    
        // Das Ergebnis in den Ziel-Datenpunkt schreiben
        setState('0_userdata.0.meine_bar', barSVG);
    });
    

    Hinweis zu anderen Visualisierungen:
    Dieses Skript generiert sauberen SVG-Code. Dadurch sollte es nicht nur in der klassischen vis, sondern auch in anderen Visualisierungen innerhalb von ioBroker problemlos funktionieren.

    Solltest du das Skript in einem anderen Adapter einsetzen und Probleme bei der Darstellung haben, empfehle ich, kurz im jeweiligen Unterforum des Adapters nachzufragen.

    Viel Erfolg beim Einbinden in dein Dashboard. Wer mag, kann sich auch die HTML-Version ansehen.

    Ro75.

    EDIT Version 1.0.1

    • es kann nun explizit die Höhe (height) und die Breite (width) mit übergeben werden

    EDIT Version 1.0.2

    • unter Umstanden wurde der Wert vom Label falsch dargestellt (Nachkommastellen / Rundung)

    SERVER = Beelink U59 16GB DDR4 RAM 512GB SSD, FB 7490, FritzDect 200+301+440, ConBee II, Zigbee Aqara Sensoren + NOUS A1Z, NOUS A1T, Philips Hue ** ioBroker, REDIS, influxdb2, Grafana, PiHole, Plex-Mediaserver, paperless-ngx (Docker), MariaDB + phpmyadmin *** VIS-Runtime = Intel NUC 8GB RAM 128GB SSD + 24" Touchscreen

    1 Antwort Letzte Antwort
    0
    • skvarelS Online
      skvarelS Online
      skvarel
      Developer
      schrieb am zuletzt editiert von
      #2

      Das geht aber doch auch ganz ohne Script mit unserem inventwo Slider für VIS2.

      #TeamInventwo
      Unsere Adapter:
      Autodarts, FoxESS, Enpal, Life360ng, Tidy, vis-inventwo, vis-2-widgets-inventwo, vis-icontwo, vis-2-widgets-icontwo

      Wer uns mit einem Kaffee unterstützen möchte: PayPal

      1 Antwort Letzte Antwort
      0
      • Ro75R Online
        Ro75R Online
        Ro75
        schrieb am zuletzt editiert von
        #3

        Ja, das mag sein. Ich zeige Möglichkeiten damit man nicht immer 20 verschiedene Adapter installieren muss, wenn es auch mit "Basics" geht. Das ist mein Ansatz und den Teile ich.

        Ro75.

        SERVER = Beelink U59 16GB DDR4 RAM 512GB SSD, FB 7490, FritzDect 200+301+440, ConBee II, Zigbee Aqara Sensoren + NOUS A1Z, NOUS A1T, Philips Hue ** ioBroker, REDIS, influxdb2, Grafana, PiHole, Plex-Mediaserver, paperless-ngx (Docker), MariaDB + phpmyadmin *** VIS-Runtime = Intel NUC 8GB RAM 128GB SSD + 24" Touchscreen

        1 Antwort Letzte Antwort
        2
        • Ro75R Online
          Ro75R Online
          Ro75
          schrieb am zuletzt editiert von
          #4

          EDIT Version 1.0.1

          • es kann nun explizit die Höhe (height) und die Breite (width) mit übergeben werden

          Script wurde in Post #1 aktualisiert.

          Ro75.

          SERVER = Beelink U59 16GB DDR4 RAM 512GB SSD, FB 7490, FritzDect 200+301+440, ConBee II, Zigbee Aqara Sensoren + NOUS A1Z, NOUS A1T, Philips Hue ** ioBroker, REDIS, influxdb2, Grafana, PiHole, Plex-Mediaserver, paperless-ngx (Docker), MariaDB + phpmyadmin *** VIS-Runtime = Intel NUC 8GB RAM 128GB SSD + 24" Touchscreen

          1 Antwort Letzte Antwort
          1
          • Ro75R Online
            Ro75R Online
            Ro75
            schrieb am zuletzt editiert von
            #5

            EDIT Version 1.0.2

            • unter Umstanden wurde der Wert vom Label falsch dargestellt (Nachkommastellen / Rundung)

            Script wurde in Post #1 aktualisiert.

            Ro75.

            SERVER = Beelink U59 16GB DDR4 RAM 512GB SSD, FB 7490, FritzDect 200+301+440, ConBee II, Zigbee Aqara Sensoren + NOUS A1Z, NOUS A1T, Philips Hue ** ioBroker, REDIS, influxdb2, Grafana, PiHole, Plex-Mediaserver, paperless-ngx (Docker), MariaDB + phpmyadmin *** VIS-Runtime = Intel NUC 8GB RAM 128GB SSD + 24" Touchscreen

            1 Antwort Letzte Antwort
            0
            • Ro75R Online
              Ro75R Online
              Ro75
              schrieb am zuletzt editiert von Ro75
              #6

              Mal ein praktisches Anwendungsbeispiel bei mir:
              8338f67a-91b1-4e1b-8d1b-c2a3cae4c6e1-image.jpeg

              Meine Wetterview. In der Mitte haben wir die Prognosedaten für die nächsten 48 Stunden für Bewölkung, Luftfeuchtigkeit, Regenwahrscheinlichkeit und Regenmenge. Insgesamt sind das 192 SVG-Progressbar. Aber nicht als Dateien oder Datenpunkte. Das ganze ist ein Widget in dem diese 192 SVG (Base64 codiert) dargestellt werden. Alles ohne Verzögerung und ohne nennenswerter Systembelastung.
              182a1344-5db2-4047-b03b-760bf068c456-image.jpeg
              Alles was hier zu sehen ist kommt aus meinem Script "EnviroSense". Dieses Script liefert mir mit nur 3 Werten folgende Daten:

              • Wetter (Openweathermap API) - wie der Adapter - 5 Tage, 48 Stunden
              • Luftqualitätsdaten (Openweathermap API)
              • DWD Daten - wie der Adapter
              • Strahlungsdaten - wie der ODL - Adapter
              • Pollenflugdaten - wie der Adapter, aber mit mehr Infos
              • Biowetterdaten
              • jede Menge Trigger die auf Wetterereignisse reagieren
              • zusätzliche Sensorwerte durch externe Sensoren
              • jede Menge an jsons zur Visualisierung mit Grafana

              Falls an dem Script interesse besteht einfach melden und vielleicht einen Daumen hoch.

              Ro75.

              SERVER = Beelink U59 16GB DDR4 RAM 512GB SSD, FB 7490, FritzDect 200+301+440, ConBee II, Zigbee Aqara Sensoren + NOUS A1Z, NOUS A1T, Philips Hue ** ioBroker, REDIS, influxdb2, Grafana, PiHole, Plex-Mediaserver, paperless-ngx (Docker), MariaDB + phpmyadmin *** VIS-Runtime = Intel NUC 8GB RAM 128GB SSD + 24" Touchscreen

              1 Antwort Letzte Antwort
              0

              Hey! Du scheinst an dieser Unterhaltung interessiert zu sein, hast aber noch kein Konto.

              Hast du es satt, bei jedem Besuch durch die gleichen Beiträge zu scrollen? Wenn du dich für ein Konto anmeldest, kommst du immer genau dorthin zurück, wo du zuvor warst, und kannst dich über neue Antworten benachrichtigen lassen (entweder per E-Mail oder Push-Benachrichtigung). Du kannst auch Lesezeichen speichern und Beiträge positiv bewerten, um anderen Community-Mitgliedern deine Wertschätzung zu zeigen.

              Mit deinem Input könnte dieser Beitrag noch besser werden 💗

              Registrieren Anmelden
              Antworten
              • In einem neuen Thema antworten
              Anmelden zum Antworten
              • Älteste zuerst
              • Neuste zuerst
              • Meiste Stimmen


              Support us

              ioBroker
              Community Adapters
              Donate

              301

              Online

              33.0k

              Benutzer

              83.4k

              Themen

              1.3m

              Beiträge
              Community
              Impressum | Datenschutz-Bestimmungen | Nutzungsbedingungen | Einwilligungseinstellungen
              ioBroker Community 2014-2026
              logo
              • Anmelden

              • Du hast noch kein Konto? Registrieren

              • Anmelden oder registrieren, um zu suchen
              • Erster Beitrag
                Letzter Beitrag
              0
              • Home
              • Aktuell
              • Tags
              • Ungelesen 0
              • Kategorien
              • Unreplied
              • Beliebt
              • GitHub
              • Docu
              • Hilfe