diff --git a/unique_bubble_graph.yaml b/unique_bubble_graph.yaml
index 62b2d31..47dd434 100644
--- a/unique_bubble_graph.yaml
+++ b/unique_bubble_graph.yaml
@@ -1,10 +1,10 @@
unique_bubble_graph:
- name: Unique Bubble History Background Graph
- version: 1.3.1
+ name: Unique Bubble Multi History Background Graph
+ version: 2.6.0
creator: Torsten
supported:
- button
- description: Shows a Home Assistant sensor history graph as Bubble Card background.
+ description: Shows up to three Home Assistant sensor history graphs as Bubble Card background with data-point cache, persistent cache, centered loading spinner and extend-to-now.
code: |
ha-card {
position: relative !important;
@@ -43,22 +43,40 @@ unique_bubble_graph:
}
.bubble-history-background path.area {
- fill: var(--bubble-history-fill-color, var(--primary-color)) !important;
- opacity: var(--bubble-history-fill-opacity, 0.10) !important;
+ stroke: none !important;
}
.bubble-history-background path.line {
fill: none !important;
- stroke: var(--bubble-history-line-color, var(--primary-color)) !important;
- stroke-width: var(--bubble-history-line-width, 2.5) !important;
stroke-linecap: round !important;
stroke-linejoin: round !important;
- opacity: var(--bubble-history-line-opacity, 0.75) !important;
}
- .bubble-history-background path.loading-line {
- opacity: 0.35 !important;
- stroke-dasharray: 6 6 !important;
+ .bubble-history-loader {
+ position: absolute !important;
+ left: 50%;
+ top: 50%;
+ z-index: 9 !important;
+ width: 18px;
+ height: 18px;
+ margin-left: -9px;
+ margin-top: -9px;
+ border-radius: 50%;
+ border: 2px solid rgba(255, 255, 255, 0.25);
+ border-top-color: var(--primary-color);
+ animation: bubble-history-spin 0.8s linear infinite;
+ display: none;
+ pointer-events: none;
+ opacity: 0.9;
+ }
+
+ @keyframes bubble-history-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
}
.bubble-history-tooltip {
@@ -70,13 +88,27 @@ unique_bubble_graph:
background: rgba(20, 20, 20, 0.82);
color: white;
font-size: 11px;
- line-height: 1.25;
+ line-height: 1.35;
white-space: nowrap;
pointer-events: none;
transform: translate(-50%, 0);
backdrop-filter: blur(8px);
}
+ .bubble-history-tooltip-row {
+ display: flex;
+ gap: 6px;
+ align-items: center;
+ }
+
+ .bubble-history-tooltip-dot {
+ width: 7px;
+ height: 7px;
+ border-radius: 50%;
+ display: inline-block;
+ flex: 0 0 auto;
+ }
+
.bubble-history-marker {
position: absolute !important;
z-index: 8 !important;
@@ -107,57 +139,119 @@ unique_bubble_graph:
this.config.history_background_graph ||
{};
- const entity = cfg.entity || this.config.entity || "sensor.sma_tripower_x_pv_power";
-
- const entityLabel =
- cfg.label ||
- hass.states?.[entity]?.attributes?.friendly_name ||
- entity ||
- "Sensor";
-
- const unit = hass.states?.[entity]?.attributes?.unit_of_measurement || "";
-
const hoursToShow = Number(cfg.hours_to_show ?? 24);
- const lineWidth = Number(cfg.line_width ?? 2.5);
- const refreshMinutes = Number(cfg.refresh_minutes ?? 0);
- const maxPoints = Number(cfg.max_points ?? 160);
- const lineOpacity = Number(cfg.line_opacity ?? 0.75);
- const fillOpacity = Number(cfg.fill_opacity ?? 0.10);
+ const refreshMinutes = Number(cfg.refresh_minutes ?? 10);
+ const maxPoints = Number(cfg.max_points ?? 120);
const borderRadius = Number(cfg.border_radius ?? 28);
const tooltipTop = Number(cfg.tooltip_top ?? 52);
const paddingY = Number(cfg.padding_y ?? 12);
+ const valuePaddingPercent = Number(cfg.value_padding_percent ?? 8);
+ const showLoading = cfg.show_loading !== false;
+ const persistentCache = cfg.persistent_cache !== false;
+ const maxPersistentCacheAgeHours = Number(cfg.max_cache_age_hours ?? 24);
- const lineColor = cfg.line_color || "var(--primary-color)";
- const fillColor = cfg.fill_color || lineColor;
+ const extendToNow = cfg.extend_to_now !== false;
+ const extendThresholdMinutes = Number(cfg.extend_threshold_minutes ?? 2);
+
+ const defaultLineWidth = Number(cfg.line_width ?? 2.5);
+ const defaultLineOpacity = Number(cfg.line_opacity ?? 0.75);
+ const defaultFillOpacity = Number(cfg.fill_opacity ?? 0.08);
+
+ const defaultColors = [
+ cfg.graph_1_color || cfg.line_color || "var(--primary-color)",
+ cfg.graph_2_color || "#2196f3",
+ cfg.graph_3_color || "#4caf50"
+ ];
+
+ const buildGraphs = () => {
+ if (Array.isArray(cfg.graphs) && cfg.graphs.length) {
+ return cfg.graphs
+ .slice(0, 3)
+ .map((graph, index) => ({
+ entity: graph.entity,
+ label:
+ graph.label ||
+ hass.states?.[graph.entity]?.attributes?.friendly_name ||
+ graph.entity ||
+ `Graph ${index + 1}`,
+ color: graph.color || graph.line_color || defaultColors[index],
+ lineWidth: Number(graph.line_width ?? defaultLineWidth),
+ lineOpacity: Number(graph.line_opacity ?? defaultLineOpacity),
+ fillOpacity: Number(graph.fill_opacity ?? defaultFillOpacity),
+ unit: hass.states?.[graph.entity]?.attributes?.unit_of_measurement || ""
+ }))
+ .filter((graph) => graph.entity);
+ }
+
+ const graph1Entity = cfg.graph_1_entity || cfg.entity || this.config.entity || "sensor.sma_tripower_x_pv_power";
+ const graph2Entity = cfg.graph_2_entity;
+ const graph3Entity = cfg.graph_3_entity;
+
+ return [
+ {
+ entity: graph1Entity,
+ label:
+ cfg.graph_1_label ||
+ cfg.label ||
+ hass.states?.[graph1Entity]?.attributes?.friendly_name ||
+ graph1Entity ||
+ "Graph 1",
+ color: cfg.graph_1_color || cfg.line_color || defaultColors[0],
+ lineWidth: Number(cfg.graph_1_line_width ?? defaultLineWidth),
+ lineOpacity: Number(cfg.graph_1_line_opacity ?? defaultLineOpacity),
+ fillOpacity: Number(cfg.graph_1_fill_opacity ?? defaultFillOpacity),
+ unit: hass.states?.[graph1Entity]?.attributes?.unit_of_measurement || ""
+ },
+ {
+ entity: graph2Entity,
+ label:
+ cfg.graph_2_label ||
+ hass.states?.[graph2Entity]?.attributes?.friendly_name ||
+ graph2Entity ||
+ "Graph 2",
+ color: cfg.graph_2_color || defaultColors[1],
+ lineWidth: Number(cfg.graph_2_line_width ?? defaultLineWidth),
+ lineOpacity: Number(cfg.graph_2_line_opacity ?? defaultLineOpacity),
+ fillOpacity: Number(cfg.graph_2_fill_opacity ?? defaultFillOpacity),
+ unit: hass.states?.[graph2Entity]?.attributes?.unit_of_measurement || ""
+ },
+ {
+ entity: graph3Entity,
+ label:
+ cfg.graph_3_label ||
+ hass.states?.[graph3Entity]?.attributes?.friendly_name ||
+ graph3Entity ||
+ "Graph 3",
+ color: cfg.graph_3_color || defaultColors[2],
+ lineWidth: Number(cfg.graph_3_line_width ?? defaultLineWidth),
+ lineOpacity: Number(cfg.graph_3_line_opacity ?? defaultLineOpacity),
+ fillOpacity: Number(cfg.graph_3_fill_opacity ?? defaultFillOpacity),
+ unit: hass.states?.[graph3Entity]?.attributes?.unit_of_measurement || ""
+ }
+ ].filter((graph) => graph.entity);
+ };
+
+ const graphs = buildGraphs();
const host = this.shadowRoot?.querySelector("ha-card")
|| this.querySelector?.("ha-card")
|| card?.querySelector?.("ha-card")
|| card;
- if (!host || !entity) return "";
+ if (!host || !graphs.length) return "";
- host.style.setProperty("--bubble-history-line-width", `${lineWidth}`);
- host.style.setProperty("--bubble-history-line-opacity", `${lineOpacity}`);
- host.style.setProperty("--bubble-history-fill-opacity", `${fillOpacity}`);
host.style.setProperty("--bubble-history-border-radius", `${borderRadius}px`);
- host.style.setProperty("--bubble-history-line-color", lineColor);
- host.style.setProperty("--bubble-history-fill-color", fillColor);
let bg = host.querySelector(".bubble-history-background");
let tooltip = host.querySelector(".bubble-history-tooltip");
let marker = host.querySelector(".bubble-history-marker");
+ let loader = host.querySelector(".bubble-history-loader");
if (!bg) {
bg = document.createElement("div");
bg.className = "bubble-history-background";
host.prepend(bg);
-
- bg.innerHTML = `
-
- `;
+ bg.innerHTML = ``;
}
if (!tooltip) {
@@ -172,9 +266,30 @@ unique_bubble_graph:
host.appendChild(marker);
}
- const formatValue = (value) => {
- if (unit === "W" && Math.abs(value) >= 1000) {
- return `${(value / 1000).toFixed(2)} kW`;
+ if (!loader) {
+ loader = document.createElement("div");
+ loader.className = "bubble-history-loader";
+ host.appendChild(loader);
+ }
+
+ const showLoader = () => {
+ if (showLoading) loader.style.display = "block";
+ };
+
+ const hideLoader = () => {
+ loader.style.display = "none";
+ };
+
+ const formatValue = (value, unit) => {
+ if (unit === "W") {
+ if (Math.abs(value) >= 1000) {
+ return `${(value / 1000).toFixed(2)} kW`;
+ }
+ return `${Math.round(value)} W`;
+ }
+
+ if (unit === "%") {
+ return `${Math.round(value)} %`;
}
if (Number.isInteger(value)) {
@@ -193,36 +308,86 @@ unique_bubble_graph:
});
};
- const attachTooltip = (points) => {
+ const findNearestPoint = (points, targetTime) => {
+ if (!points?.length) return null;
+
+ let nearest = points[0];
+ let nearestDiff = Math.abs(points[0].t - targetTime);
+
+ for (const point of points) {
+ const diff = Math.abs(point.t - targetTime);
+ if (diff < nearestDiff) {
+ nearest = point;
+ nearestDiff = diff;
+ }
+ }
+
+ return nearest;
+ };
+
+ const extendSeriesToNow = (seriesList) => {
+ if (!extendToNow) return seriesList;
+
+ const now = Date.now();
+ const thresholdMs = extendThresholdMinutes * 60 * 1000;
+
+ return seriesList.map((series) => {
+ const points = [...(series.points || [])];
+ const lastPoint = points[points.length - 1];
+ const currentValue = Number(hass.states?.[series.entity]?.state);
+
+ if (
+ Number.isFinite(currentValue) &&
+ (!lastPoint || now - lastPoint.t > thresholdMs)
+ ) {
+ points.push({
+ t: now,
+ v: currentValue
+ });
+ }
+
+ return {
+ ...series,
+ points
+ };
+ });
+ };
+
+ const attachTooltip = (seriesList) => {
if (cfg.tooltip === false) return;
host.onmousemove = (event) => {
- if (!points?.length) return;
+ const usableSeries = seriesList.filter((series) => series.points?.length);
+ if (!usableSeries.length) return;
const rect = host.getBoundingClientRect();
const xRatio = Math.min(1, Math.max(0, (event.clientX - rect.left) / rect.width));
- const minTime = points[0].t;
- const maxTime = points[points.length - 1].t;
- const targetTime = minTime + xRatio * (maxTime - minTime);
+ const globalMinTime = Math.min(...usableSeries.map((series) => series.points[0].t));
+ const globalMaxTime = Math.max(...usableSeries.map((series) => series.points[series.points.length - 1].t));
+ const targetTime = globalMinTime + xRatio * (globalMaxTime - globalMinTime);
- let nearest = points[0];
- let nearestDiff = Math.abs(points[0].t - targetTime);
+ const rows = usableSeries.map((series) => {
+ const nearest = findNearestPoint(series.points, targetTime);
+ if (!nearest) return "";
- for (const point of points) {
- const diff = Math.abs(point.t - targetTime);
- if (diff < nearestDiff) {
- nearest = point;
- nearestDiff = diff;
- }
- }
+ return `
+
+
+ ${series.label}: ${formatValue(nearest.v, series.unit)}
+
+ `;
+ }).join("");
- const x = ((nearest.t - minTime) / (maxTime - minTime || 1)) * rect.width;
- const safeX = Math.min(rect.width - 55, Math.max(55, x));
+ const firstNearest = findNearestPoint(usableSeries[0].points, targetTime);
+ const tooltipTime = firstNearest ? firstNearest.t : targetTime;
+
+ const x = ((targetTime - globalMinTime) / (globalMaxTime - globalMinTime || 1)) * rect.width;
+ const safeX = Math.min(rect.width - 60, Math.max(60, x));
tooltip.innerHTML = `
- ${entityLabel}: ${formatValue(nearest.v)}
- ${formatTime(nearest.t)}
+ ${rows}
+ ${formatTime(tooltipTime)}
`;
tooltip.style.display = "block";
@@ -239,66 +404,34 @@ unique_bubble_graph:
};
};
- if (!hass?.states?.[entity]) {
- console.warn(`Bubble history graph: entity not found: ${entity}`);
- return "";
- }
+ const renderGraph = (seriesList) => {
+ const usableSeries = extendSeriesToNow(seriesList)
+ .filter((series) => series.points?.length >= 2);
- if (!hass?.fetchWithAuth) {
- console.warn("Bubble history graph: hass.fetchWithAuth not available");
- return "";
- }
+ if (!usableSeries.length) return false;
- if (bg.dataset.loading === "true") return "";
- bg.dataset.loading = "true";
+ const width = 100;
+ const height = 100;
- const end = new Date();
- const start = new Date(end.getTime() - hoursToShow * 60 * 60 * 1000);
+ const globalMinTime = Math.min(...usableSeries.map((series) => series.points[0].t));
+ const globalMaxTime = Math.max(...usableSeries.map((series) => series.points[series.points.length - 1].t));
- const url =
- `/api/history/period/${encodeURIComponent(start.toISOString())}` +
- `?filter_entity_id=${encodeURIComponent(entity)}` +
- `&end_time=${encodeURIComponent(end.toISOString())}` +
- `&minimal_response`;
+ const buildPath = (series) => {
+ const rawMinValue = Math.min(...series.points.map((point) => point.v));
+ const rawMaxValue = Math.max(...series.points.map((point) => point.v));
- hass.fetchWithAuth(url)
- .then((response) => response.json())
- .then((history) => {
- bg.dataset.loading = "false";
+ const rawRange = rawMaxValue - rawMinValue;
+ const safeRange = rawRange || Math.max(Math.abs(rawMaxValue), 1);
+ const valuePadding = safeRange * (valuePaddingPercent / 100);
- const states = Array.isArray(history) ? history.flat() : [];
+ const minValue = rawMinValue - valuePadding;
+ const maxValue = rawMaxValue + valuePadding;
- let points = states
- .map((state) => ({
- t: new Date(state.lu || state.lc || state.last_updated || state.last_changed).getTime(),
- v: Number(state.s ?? state.state)
- }))
- .filter((point) => Number.isFinite(point.t) && Number.isFinite(point.v))
- .sort((a, b) => a.t - b.t);
-
- if (points.length < 2) {
- console.warn(`Bubble history graph: not enough points: ${points.length}`);
- return;
- }
-
- if (points.length > maxPoints) {
- const step = Math.ceil(points.length / maxPoints);
- points = points.filter((_, index) => index % step === 0);
- }
-
- const width = 100;
- const height = 100;
-
- const minTime = Math.min(...points.map((point) => point.t));
- const maxTime = Math.max(...points.map((point) => point.t));
- const minValue = Math.min(...points.map((point) => point.v));
- const maxValue = Math.max(...points.map((point) => point.v));
-
- const timeRange = maxTime - minTime || 1;
+ const timeRange = globalMaxTime - globalMinTime || 1;
const valueRange = maxValue - minValue || 1;
- const xy = points.map((point) => {
- const x = ((point.t - minTime) / timeRange) * width;
+ const xy = series.points.map((point) => {
+ const x = ((point.t - globalMinTime) / timeRange) * width;
const y =
height -
paddingY -
@@ -308,51 +441,362 @@ unique_bubble_graph:
return [x, y];
});
- const linePath = xy
+ return xy
.map(([x, y], index) =>
`${index === 0 ? "M" : "L"} ${x.toFixed(2)} ${y.toFixed(2)}`
)
.join(" ");
+ };
+ const paths = usableSeries.map((series, index) => {
+ const linePath = buildPath(series);
const areaPath = `${linePath} L ${width} ${height} L 0 ${height} Z`;
- const svg = `
-
+ const area = series.fillOpacity > 0
+ ? ``
+ : "";
+
+ const line = `
+
`;
- bg.innerHTML = svg;
- attachTooltip(points);
+ return `${area}${line}`;
+ }).join("");
+
+ bg.innerHTML = `
+
+ `;
+
+ attachTooltip(usableSeries);
+ return true;
+ };
+
+ if (!hass?.fetchWithAuth) {
+ console.warn("Bubble history graph: hass.fetchWithAuth not available");
+ hideLoader();
+ return "";
+ }
+
+ const validGraphs = graphs.filter((graph) => {
+ if (!hass?.states?.[graph.entity]) {
+ console.warn(`Bubble history graph: entity not found: ${graph.entity}`);
+ return false;
+ }
+ return true;
+ });
+
+ if (!validGraphs.length) {
+ hideLoader();
+ return "";
+ }
+
+ const dataCacheParts = validGraphs.map((graph) => graph.entity).join("|");
+
+ const cacheKey = [
+ "unique-bubble-graph-data-v260",
+ dataCacheParts,
+ hoursToShow,
+ maxPoints
+ ].join("::");
+
+ const storageKey = `unique-bubble-graph-data-cache:${cacheKey}`;
+
+ window.__uniqueBubbleGraphDataCache = window.__uniqueBubbleGraphDataCache || {};
+
+ const readPersistentCache = () => {
+ if (!persistentCache) return null;
+
+ try {
+ const raw = localStorage.getItem(storageKey);
+ if (!raw) return null;
+
+ const parsed = JSON.parse(raw);
+
+ if (!Array.isArray(parsed?.seriesList) || !parsed?.timestamp) {
+ return null;
+ }
+
+ const maxAgeMs = maxPersistentCacheAgeHours * 60 * 60 * 1000;
+
+ if (Date.now() - parsed.timestamp > maxAgeMs) {
+ localStorage.removeItem(storageKey);
+ return null;
+ }
+
+ return parsed;
+ } catch (error) {
+ console.warn("Bubble history graph persistent data cache read error:", error);
+ return null;
+ }
+ };
+
+ const writePersistentCache = (data) => {
+ if (!persistentCache) return;
+
+ try {
+ localStorage.setItem(storageKey, JSON.stringify({
+ timestamp: data.timestamp,
+ seriesList: data.seriesList
+ }));
+ } catch (error) {
+ console.warn("Bubble history graph persistent data cache write error:", error);
+ }
+ };
+
+ let cache =
+ window.__uniqueBubbleGraphDataCache[cacheKey] ||
+ readPersistentCache();
+
+ if (cache?.seriesList) {
+ renderGraph(cache.seriesList);
+
+ window.__uniqueBubbleGraphDataCache[cacheKey] = {
+ ...cache,
+ loading: false
+ };
+ }
+
+ if (cache?.timestamp && Date.now() - cache.timestamp < refreshMinutes * 60 * 1000) {
+ hideLoader();
+ return "";
+ }
+
+ if (window.__uniqueBubbleGraphDataCache[cacheKey]?.loading) {
+ if (!cache?.seriesList) showLoader();
+ return "";
+ }
+
+ showLoader();
+
+ window.__uniqueBubbleGraphDataCache[cacheKey] = {
+ ...(window.__uniqueBubbleGraphDataCache[cacheKey] || {}),
+ ...(cache || {}),
+ loading: true
+ };
+
+ const end = new Date();
+ const start = new Date(end.getTime() - hoursToShow * 60 * 60 * 1000);
+
+ const fetchHistory = (graph) => {
+ const url =
+ `/api/history/period/${encodeURIComponent(start.toISOString())}` +
+ `?filter_entity_id=${encodeURIComponent(graph.entity)}` +
+ `&end_time=${encodeURIComponent(end.toISOString())}` +
+ `&minimal_response`;
+
+ return hass.fetchWithAuth(url)
+ .then((response) => response.json())
+ .then((history) => {
+ const states = Array.isArray(history) ? history.flat() : [];
+
+ let points = states
+ .map((state) => ({
+ t: new Date(state.lu || state.lc || state.last_updated || state.last_changed).getTime(),
+ v: Number(state.s ?? state.state)
+ }))
+ .filter((point) => Number.isFinite(point.t) && Number.isFinite(point.v))
+ .sort((a, b) => a.t - b.t);
+
+ if (points.length > maxPoints) {
+ const originalLastPoint = points[points.length - 1];
+ const step = Math.ceil(points.length / maxPoints);
+
+ points = points.filter((_, index) => index % step === 0);
+
+ if (
+ originalLastPoint &&
+ points[points.length - 1]?.t !== originalLastPoint.t
+ ) {
+ points.push(originalLastPoint);
+ }
+ }
+
+ return {
+ ...graph,
+ points
+ };
+ });
+ };
+
+ Promise.all(validGraphs.map(fetchHistory))
+ .then((seriesList) => {
+ hideLoader();
+
+ const usableSeries = seriesList.filter((series) => series.points.length >= 2);
+
+ if (!usableSeries.length) {
+ console.warn("Bubble history graph: not enough points for all graphs");
+
+ window.__uniqueBubbleGraphDataCache[cacheKey] = {
+ ...(window.__uniqueBubbleGraphDataCache[cacheKey] || {}),
+ loading: false,
+ timestamp: Date.now()
+ };
+
+ return;
+ }
+
+ const newCache = {
+ timestamp: Date.now(),
+ loading: false,
+ seriesList: usableSeries
+ };
+
+ window.__uniqueBubbleGraphDataCache[cacheKey] = newCache;
+ writePersistentCache(newCache);
+
+ renderGraph(usableSeries);
})
.catch((error) => {
- bg.dataset.loading = "false";
+ hideLoader();
+
+ window.__uniqueBubbleGraphDataCache[cacheKey] = {
+ ...(window.__uniqueBubbleGraphDataCache[cacheKey] || {}),
+ loading: false,
+ timestamp: Date.now()
+ };
+
console.warn("Bubble history graph error:", error);
});
return "";
})()}
editor:
- - name: entity
- label: Graph Sensor
- selector:
- entity:
- domain: sensor
- - name: label
- label: Tooltip Label
- selector:
- text: null
- name: hours_to_show
- label: Stunden anzeigen
+ label: Allgemein · Stunden anzeigen
selector:
number:
min: 1
max: 168
step: 1
mode: box
+ - name: refresh_minutes
+ label: Allgemein · Aktualisierung Minuten
+ selector:
+ number:
+ min: 1
+ max: 120
+ step: 1
+ mode: box
+ - name: persistent_cache
+ label: Allgemein · Cache nach F5 behalten
+ selector:
+ boolean: null
+ - name: max_cache_age_hours
+ label: Allgemein · Max. Cache-Alter Stunden
+ selector:
+ number:
+ min: 1
+ max: 168
+ step: 1
+ mode: box
+ - name: show_loading
+ label: Allgemein · Lade-Kringel anzeigen
+ selector:
+ boolean: null
+
+ - name: graph_1_entity
+ label: ① Graph · Sensor
+ selector:
+ entity:
+ domain: sensor
+ - name: graph_1_label
+ label: ① Graph · Label
+ selector:
+ text: null
+ - name: graph_1_color
+ label: ① Graph · Farbe
+ selector:
+ text: null
+ - name: graph_1_line_width
+ label: ① Graph · Linien Dicke
+ selector:
+ number:
+ min: 0.5
+ max: 10
+ step: 0.1
+ mode: box
+ - name: graph_1_fill_opacity
+ label: ① Graph · Fläche
+ selector:
+ number:
+ min: 0
+ max: 1
+ step: 0.05
+ mode: slider
+
+ - name: graph_2_entity
+ label: ② Graph · Sensor
+ selector:
+ entity:
+ domain: sensor
+ - name: graph_2_label
+ label: ② Graph · Label
+ selector:
+ text: null
+ - name: graph_2_color
+ label: ② Graph · Farbe
+ selector:
+ text: null
+ - name: graph_2_line_width
+ label: ② Graph · Linien Dicke
+ selector:
+ number:
+ min: 0.5
+ max: 10
+ step: 0.1
+ mode: box
+ - name: graph_2_fill_opacity
+ label: ② Graph · Fläche
+ selector:
+ number:
+ min: 0
+ max: 1
+ step: 0.05
+ mode: slider
+
+ - name: graph_3_entity
+ label: ③ Graph · Sensor
+ selector:
+ entity:
+ domain: sensor
+ - name: graph_3_label
+ label: ③ Graph · Label
+ selector:
+ text: null
+ - name: graph_3_color
+ label: ③ Graph · Farbe
+ selector:
+ text: null
+ - name: graph_3_line_width
+ label: ③ Graph · Linien Dicke
+ selector:
+ number:
+ min: 0.5
+ max: 10
+ step: 0.1
+ mode: box
+ - name: graph_3_fill_opacity
+ label: ③ Graph · Fläche
+ selector:
+ number:
+ min: 0
+ max: 1
+ step: 0.05
+ mode: slider
+
- name: line_width
- label: Linien Dicke
+ label: Anzeige · Standard Linien Dicke
selector:
number:
min: 0.5
@@ -360,7 +804,7 @@ unique_bubble_graph:
step: 0.1
mode: box
- name: line_opacity
- label: Linien Sichtbarkeit
+ label: Anzeige · Linien Sichtbarkeit
selector:
number:
min: 0
@@ -368,35 +812,23 @@ unique_bubble_graph:
step: 0.05
mode: slider
- name: fill_opacity
- label: Flächen Sichtbarkeit
+ label: Anzeige · Standard Fläche
selector:
number:
min: 0
max: 1
step: 0.05
mode: slider
- - name: max_points
- label: Max. Datenpunkte
- selector:
- number:
- min: 20
- max: 500
- step: 10
- mode: box
- - name: tooltip
- label: Tooltip anzeigen
- selector:
- boolean: null
- - name: tooltip_top
- label: Tooltip Position oben
+ - name: value_padding_percent
+ label: Anzeige · Abstand oben/unten Prozent
selector:
number:
min: 0
- max: 200
+ max: 30
step: 1
mode: box
- name: padding_y
- label: Graph Abstand oben/unten
+ label: Anzeige · Graph Abstand oben/unten
selector:
number:
min: 0
@@ -404,10 +836,43 @@ unique_bubble_graph:
step: 1
mode: box
- name: border_radius
- label: Eckenradius
+ label: Anzeige · Eckenradius
selector:
number:
min: 0
max: 60
step: 1
mode: box
+
+ - name: extend_to_now
+ label: Erweitert · Linie bis jetzt verlängern
+ selector:
+ boolean: null
+ - name: extend_threshold_minutes
+ label: Erweitert · Verlängern nach Minuten
+ selector:
+ number:
+ min: 0
+ max: 60
+ step: 1
+ mode: box
+ - name: max_points
+ label: Erweitert · Max. Datenpunkte
+ selector:
+ number:
+ min: 20
+ max: 500
+ step: 10
+ mode: box
+ - name: tooltip
+ label: Erweitert · Tooltip anzeigen
+ selector:
+ boolean: null
+ - name: tooltip_top
+ label: Erweitert · Tooltip Position oben
+ selector:
+ number:
+ min: 0
+ max: 200
+ step: 1
+ mode: box