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" 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)
-
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.
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