NEWS
Dynamische SVG-Progress-Bar
-
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.

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" Width Number / null Breite des SVG. null= automatische Standardbreite (80 / 15 je nach Orientierung).Height Number / null Höhe des SVG. null= automatische Standardhöhe (15 / 80 je nach Orientierung).value Number Aktueller Wert, wird relativ zu maxValueberechnet (z. B. 0–100).maxValue Number Standard = 100. Kann jeder andere Wert sein, z. B. 3000 für Zeitangaben. progressColor String Farbe des Fortschrittsbalkens (Hex, RGB oder RGBA). backgroundColor String Hintergrundfarbe des Balkens (Hex, RGB, RGBA oder "transparent"). borderWidth 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". Wird für alle Labels verwendet. labelColor String Textfarbe (Hex, RGB oder RGBA). labelShadow Boolean truefür dezenten Textschatten.labelSuffix String Text hinter dem Hauptlabel‑Wert, z. B. "%". gradient Boolean truefür automatischen Farbverlauf.effect String "glanz"für Glanz‑Effekt oder"none".decimalPlaces Number Anzahl Nachkommastellen für das Hauptlabel. fontWeight String "normal","bold","semibold"oder numerische Werte (400,500,600,700).fontStyle String "normal","italic"oder"oblique".leftLabelText String Text für das linke Label (nur horizontal). Abstand: 10px von links. rightLabelText String Text für das rechte Label (nur horizontal). Abstand: 10px von rechts. showMainLabel Boolean true= Hauptlabel anzeigen,false= Hauptlabel ausblenden.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.4 - 16.07.2026 //Ersteller Ro75. //Voraussetzungen (Version 1.0.4 getestet mit) //NodeJS: 22.22.x //Javascript-Adapter: 9.0.18 //Admin-Adapter: 7.8.23 //js-controler: 7.2.2 function buildProgressBarSvg(orientation, Width = null, Height = null, value, maxValue = 100, progressColor, backgroundColor, borderWidth, borderColor, borderRadius, labelFont, labelSize, labelColor, labelShadow, labelSuffix, gradient, effect, decimalPlaces = 0, fontWeight = "normal", fontStyle = "normal", leftLabelText = "", rightLabelText = "", showMainLabel = true) { const uid = "pb_" + Math.random().toString(36).slice(2); 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 = uid + "_grad"; 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 = borderWidth; const innerY = borderWidth; const innerW = svgW - borderWidth * 2; const innerH = svgH - borderWidth * 2; const outerRadius = borderRadius; const innerRadius = Math.max(0, outerRadius - borderWidth); const effectiveRadius = innerRadius - 0.3; const clipId = uid + "_clip"; const glossClipId = uid + "_gloss"; 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 pctFixed = Math.round(pct * 1000000) / 1000000; const labelText = pctFixed.toFixed(decimalPlaces) + " " + 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} !important; font-size:${labelSize} !important; font-weight:${fontWeight} !important; font-style:${fontStyle} !important; color:${labelColor} !important; ${shadow} white-space:nowrap; pointer-events:none; "> ${showMainLabel ? labelText : ""} </div> ${orientation === "horizontal" && leftLabelText !== "" ? ` <div xmlns="http://www.w3.org/1999/xhtml" style=" position:absolute; top:50%; left:10px; transform:translate(0,-50%); font-family:${labelFont} !important; font-size:${labelSize} !important; font-weight:${fontWeight} !important; font-style:${fontStyle} !important; color:${labelColor} !important; ${shadow} white-space:nowrap; pointer-events:none; text-align:left; "> ${leftLabelText} </div> ` : ""} ${orientation === "horizontal" && rightLabelText !== "" ? ` <div xmlns="http://www.w3.org/1999/xhtml" style=" position:absolute; top:50%; right:10px; transform:translate(0,-50%); font-family:${labelFont} !important; font-size:${labelSize} !important; font-weight:${fontWeight} !important; font-style:${fontStyle} !important; color:${labelColor} !important; ${shadow} white-space:nowrap; pointer-events:none; text-align:right; "> ${rightLabelText} </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", // orientation 200, // Width 40, // Height obj.state.val, // value 100, // maxValue "#004400", // progressColor "#333333", // backgroundColor 0.5, // borderWidth "white", // borderColor 5, // borderRadius "Arial", // labelFont "6px", // labelSize "#ffffff", // labelColor true, // labelShadow "%", // labelSuffix true, // gradient "none", // effect 0, // decimalPlaces "normal", // fontWeight "normal", // fontStyle "", // leftLabelText "", // rightLabelText true // showMainLabel ); // 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)
EDIT Version 1.0.4
*erweiterte Konfiguration:
Neuer Parameter Zweck decimalPlaces Anzahl Nachkommastellen für das Hauptlabel fontWeight Schriftstärke (normal, bold, semibold, 400–700) fontStyle Schriftstil (normal, italic, oblique) leftLabelText Text für das linke Label (nur horizontal) rightLabelText Text für das rechte Label (nur horizontal) showMainLabel Hauptlabel ein-/ausschalten - Aufgrund der neuen Parameter, wurde die Reihenfolge für die Übergabe der Parameter in der Funktion neu angeordnet
-
Mal ein praktisches Anwendungsbeispiel bei mir:

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.

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.
-
Version Version 1.0.4
!!!ACHTUNG WICHTIG!!!
Wenn schon eine vorherige Version vom Script genutzt wurde, MÜSSEN mit dieser Version die Übergabewerte im Funktionskopf anpasst werden - kurz gesagt: die Reihenfolge hat sich geändert!
*erweiterte Konfiguration:
Neuer Parameter Zweck decimalPlaces Anzahl Nachkommastellen für das Hauptlabel fontWeight Schriftstärke (normal, bold, semibold, 400–700) fontStyle Schriftstil (normal, italic, oblique) leftLabelText Text für das linke Label (nur horizontal) rightLabelText Text für das rechte Label (nur horizontal) showMainLabel Hauptlabel ein-/ausschalten - Aufgrund der neuen Parameter, wurde die Reihenfolge für die Übergabe der Parameter in der Funktion neu angeordnet
Alle weiteren Informationen findest Du im Post #1. Das Script und der Beispielaufruf, sowie die Parameterbeschreibung sind aktualisiert.
Ro75.
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