unique_bubble_graph.yaml aktualisiert
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
unique_bubble_graph:
|
||||
name: Unique Bubble Multi History Background Graph
|
||||
version: 2.6.0
|
||||
version: 2.8.6
|
||||
creator: Torsten
|
||||
supported:
|
||||
- button
|
||||
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.
|
||||
description: Shows up to three explicitly configured Home Assistant sensor history graphs as Bubble Card background with data-point cache, persistent cache, centered loading spinner, extend-to-now, peak-preserving downsampling and optional same-scale mode, fixed scale limits, percent-of-max value conversion and nested expandable editor settings support and RGB color picker array support and isolated per-graph color settings and cache-safe live style refresh.
|
||||
code: |
|
||||
ha-card {
|
||||
position: relative !important;
|
||||
@@ -134,11 +134,34 @@ unique_bubble_graph:
|
||||
}
|
||||
|
||||
${(() => {
|
||||
const cfg =
|
||||
const rawCfg =
|
||||
this.config.unique_bubble_graph ||
|
||||
this.config.history_background_graph ||
|
||||
{};
|
||||
|
||||
const graphSettings = [
|
||||
rawCfg.graph_1_settings || {},
|
||||
rawCfg.graph_2_settings || {},
|
||||
rawCfg.graph_3_settings || {}
|
||||
];
|
||||
|
||||
const cfg = {
|
||||
...(rawCfg.general_settings || {}),
|
||||
...(rawCfg.scale_settings || {}),
|
||||
...(rawCfg.advanced_settings || {}),
|
||||
...rawCfg
|
||||
};
|
||||
|
||||
const getGraphSetting = (index, key, fallback = undefined) => {
|
||||
const prefix = `graph_${index + 1}`;
|
||||
const group = graphSettings[index] || {};
|
||||
|
||||
return group[`${prefix}_${key}`] ??
|
||||
group[key] ??
|
||||
cfg[`${prefix}_${key}`] ??
|
||||
fallback;
|
||||
};
|
||||
|
||||
const hoursToShow = Number(cfg.hours_to_show ?? 24);
|
||||
const refreshMinutes = Number(cfg.refresh_minutes ?? 10);
|
||||
const maxPoints = Number(cfg.max_points ?? 120);
|
||||
@@ -153,80 +176,172 @@ unique_bubble_graph:
|
||||
const extendToNow = cfg.extend_to_now !== false;
|
||||
const extendThresholdMinutes = Number(cfg.extend_threshold_minutes ?? 2);
|
||||
|
||||
const sameScale = cfg.same_scale === true;
|
||||
const numberOrNull = (value) => {
|
||||
if (value === undefined || value === null || value === "") return null;
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) ? number : null;
|
||||
};
|
||||
const sameScaleMin = numberOrNull(cfg.same_scale_min);
|
||||
const sameScaleMax = numberOrNull(cfg.same_scale_max);
|
||||
|
||||
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 normalizeColor = (color, fallback = "var(--primary-color)") => {
|
||||
if (Array.isArray(color) && color.length >= 3) {
|
||||
const clamp = (value) => Math.max(0, Math.min(255, Math.round(Number(value) || 0)));
|
||||
const r = clamp(color[0]);
|
||||
const g = clamp(color[1]);
|
||||
const b = clamp(color[2]);
|
||||
const a = color.length >= 4 ? Number(color[3]) : null;
|
||||
|
||||
if (Number.isFinite(a)) {
|
||||
return `rgba(${r}, ${g}, ${b}, ${Math.max(0, Math.min(1, a))})`;
|
||||
}
|
||||
|
||||
return `rgb(${r}, ${g}, ${b})`;
|
||||
}
|
||||
|
||||
if (typeof color !== "string") return fallback;
|
||||
|
||||
const trimmed = color.trim();
|
||||
|
||||
if (!trimmed) return fallback;
|
||||
|
||||
if (
|
||||
trimmed.startsWith("#") ||
|
||||
trimmed.startsWith("rgb(") ||
|
||||
trimmed.startsWith("rgba(") ||
|
||||
trimmed.startsWith("hsl(") ||
|
||||
trimmed.startsWith("hsla(") ||
|
||||
trimmed.startsWith("var(")
|
||||
) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
if (/^[0-9a-fA-F]{3}$/.test(trimmed) || /^[0-9a-fA-F]{6}$/.test(trimmed)) {
|
||||
return `#${trimmed}`;
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
};
|
||||
|
||||
const defaultColors = [
|
||||
cfg.graph_1_color || cfg.line_color || "var(--primary-color)",
|
||||
cfg.graph_2_color || "#2196f3",
|
||||
cfg.graph_3_color || "#4caf50"
|
||||
"var(--primary-color)",
|
||||
"#2196f3",
|
||||
"#4caf50"
|
||||
];
|
||||
|
||||
const normalizeValueMode = (value) => {
|
||||
return value === "percent_of_max" ? "percent_of_max" : "default";
|
||||
};
|
||||
|
||||
const getGraphScaleConfig = (graph, index) => {
|
||||
const valueMode = normalizeValueMode(
|
||||
graph?.value_mode ??
|
||||
graph?.valueMode ??
|
||||
getGraphSetting(index, "value_mode")
|
||||
);
|
||||
const maxValue = numberOrNull(
|
||||
graph?.max_value ??
|
||||
graph?.maxValue ??
|
||||
getGraphSetting(index, "max_value")
|
||||
);
|
||||
const effectiveValueMode =
|
||||
valueMode === "percent_of_max" &&
|
||||
Number.isFinite(maxValue) &&
|
||||
maxValue > 0
|
||||
? "percent_of_max"
|
||||
: "default";
|
||||
|
||||
return {
|
||||
valueMode: effectiveValueMode,
|
||||
maxValue
|
||||
};
|
||||
};
|
||||
|
||||
const buildGraphs = () => {
|
||||
if (Array.isArray(cfg.graphs) && cfg.graphs.length) {
|
||||
return cfg.graphs
|
||||
.slice(0, 3)
|
||||
.map((graph, index) => ({
|
||||
.map((graph, index) => {
|
||||
const scaleConfig = getGraphScaleConfig(graph, index);
|
||||
|
||||
return {
|
||||
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],
|
||||
color: normalizeColor(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 || ""
|
||||
}))
|
||||
rawUnit: hass.states?.[graph.entity]?.attributes?.unit_of_measurement || "",
|
||||
unit: scaleConfig.valueMode === "percent_of_max" ? "%" : (hass.states?.[graph.entity]?.attributes?.unit_of_measurement || ""),
|
||||
valueMode: scaleConfig.valueMode,
|
||||
maxValue: scaleConfig.maxValue
|
||||
};
|
||||
})
|
||||
.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;
|
||||
const graph1Entity = getGraphSetting(0, "entity");
|
||||
const graph2Entity = getGraphSetting(1, "entity");
|
||||
const graph3Entity = getGraphSetting(2, "entity");
|
||||
|
||||
return [
|
||||
{
|
||||
entity: graph1Entity,
|
||||
label:
|
||||
cfg.graph_1_label ||
|
||||
getGraphSetting(0, "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 || ""
|
||||
color: normalizeColor(getGraphSetting(0, "color") || cfg.line_color, defaultColors[0]),
|
||||
lineWidth: Number(getGraphSetting(0, "line_width", defaultLineWidth)),
|
||||
lineOpacity: Number(getGraphSetting(0, "line_opacity", defaultLineOpacity)),
|
||||
fillOpacity: Number(getGraphSetting(0, "fill_opacity", defaultFillOpacity)),
|
||||
rawUnit: hass.states?.[graph1Entity]?.attributes?.unit_of_measurement || "",
|
||||
unit: getGraphScaleConfig(null, 0).valueMode === "percent_of_max" ? "%" : (hass.states?.[graph1Entity]?.attributes?.unit_of_measurement || ""),
|
||||
valueMode: getGraphScaleConfig(null, 0).valueMode,
|
||||
maxValue: getGraphScaleConfig(null, 0).maxValue
|
||||
},
|
||||
{
|
||||
entity: graph2Entity,
|
||||
label:
|
||||
cfg.graph_2_label ||
|
||||
getGraphSetting(1, "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 || ""
|
||||
color: normalizeColor(getGraphSetting(1, "color"), defaultColors[1]),
|
||||
lineWidth: Number(getGraphSetting(1, "line_width", defaultLineWidth)),
|
||||
lineOpacity: Number(getGraphSetting(1, "line_opacity", defaultLineOpacity)),
|
||||
fillOpacity: Number(getGraphSetting(1, "fill_opacity", defaultFillOpacity)),
|
||||
rawUnit: hass.states?.[graph2Entity]?.attributes?.unit_of_measurement || "",
|
||||
unit: getGraphScaleConfig(null, 1).valueMode === "percent_of_max" ? "%" : (hass.states?.[graph2Entity]?.attributes?.unit_of_measurement || ""),
|
||||
valueMode: getGraphScaleConfig(null, 1).valueMode,
|
||||
maxValue: getGraphScaleConfig(null, 1).maxValue
|
||||
},
|
||||
{
|
||||
entity: graph3Entity,
|
||||
label:
|
||||
cfg.graph_3_label ||
|
||||
getGraphSetting(2, "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 || ""
|
||||
color: normalizeColor(getGraphSetting(2, "color"), defaultColors[2]),
|
||||
lineWidth: Number(getGraphSetting(2, "line_width", defaultLineWidth)),
|
||||
lineOpacity: Number(getGraphSetting(2, "line_opacity", defaultLineOpacity)),
|
||||
fillOpacity: Number(getGraphSetting(2, "fill_opacity", defaultFillOpacity)),
|
||||
rawUnit: hass.states?.[graph3Entity]?.attributes?.unit_of_measurement || "",
|
||||
unit: getGraphScaleConfig(null, 2).valueMode === "percent_of_max" ? "%" : (hass.states?.[graph3Entity]?.attributes?.unit_of_measurement || ""),
|
||||
valueMode: getGraphScaleConfig(null, 2).valueMode,
|
||||
maxValue: getGraphScaleConfig(null, 2).maxValue
|
||||
}
|
||||
].filter((graph) => graph.entity);
|
||||
};
|
||||
@@ -299,6 +414,31 @@ unique_bubble_graph:
|
||||
return `${value.toFixed(1)} ${unit}`.trim();
|
||||
};
|
||||
|
||||
const formatTooltipValue = (point, series) => {
|
||||
const value = formatValue(point.v, series.unit);
|
||||
|
||||
if (
|
||||
series.valueMode === "percent_of_max" &&
|
||||
Number.isFinite(point.rawV)
|
||||
) {
|
||||
return `${value} · ${formatValue(point.rawV, series.rawUnit)}`;
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
const transformValueForGraph = (value, graph) => {
|
||||
if (
|
||||
graph.valueMode === "percent_of_max" &&
|
||||
Number.isFinite(graph.maxValue) &&
|
||||
graph.maxValue > 0
|
||||
) {
|
||||
return (value / graph.maxValue) * 100;
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
const formatTime = (timestamp) => {
|
||||
return new Date(timestamp).toLocaleString("de-DE", {
|
||||
day: "2-digit",
|
||||
@@ -325,6 +465,42 @@ unique_bubble_graph:
|
||||
return nearest;
|
||||
};
|
||||
|
||||
const downsampleMinMax = (points, maxPoints) => {
|
||||
if (!Array.isArray(points) || points.length <= maxPoints) return points;
|
||||
|
||||
const firstPoint = points[0];
|
||||
const lastPoint = points[points.length - 1];
|
||||
const bucketCount = Math.max(1, Math.floor((maxPoints - 2) / 2));
|
||||
const bucketSize = Math.ceil(points.length / bucketCount);
|
||||
const sampled = [firstPoint];
|
||||
|
||||
for (let index = 0; index < points.length; index += bucketSize) {
|
||||
const bucket = points.slice(index, index + bucketSize);
|
||||
if (!bucket.length) continue;
|
||||
|
||||
const min = bucket.reduce((a, b) => b.v < a.v ? b : a);
|
||||
const max = bucket.reduce((a, b) => b.v > a.v ? b : a);
|
||||
|
||||
if (min.t <= max.t) {
|
||||
sampled.push(min, max);
|
||||
} else {
|
||||
sampled.push(max, min);
|
||||
}
|
||||
}
|
||||
|
||||
sampled.push(lastPoint);
|
||||
|
||||
return sampled
|
||||
.filter((point, index, array) =>
|
||||
point &&
|
||||
Number.isFinite(point.t) &&
|
||||
Number.isFinite(point.v) &&
|
||||
array.findIndex((item) => item.t === point.t && item.v === point.v) === index
|
||||
)
|
||||
.sort((a, b) => a.t - b.t)
|
||||
.slice(0, maxPoints);
|
||||
};
|
||||
|
||||
const extendSeriesToNow = (seriesList) => {
|
||||
if (!extendToNow) return seriesList;
|
||||
|
||||
@@ -334,15 +510,18 @@ unique_bubble_graph:
|
||||
return seriesList.map((series) => {
|
||||
const points = [...(series.points || [])];
|
||||
const lastPoint = points[points.length - 1];
|
||||
const currentValue = Number(hass.states?.[series.entity]?.state);
|
||||
const currentRawValue = Number(hass.states?.[series.entity]?.state);
|
||||
const currentValue = transformValueForGraph(currentRawValue, series);
|
||||
|
||||
if (
|
||||
Number.isFinite(currentRawValue) &&
|
||||
Number.isFinite(currentValue) &&
|
||||
(!lastPoint || now - lastPoint.t > thresholdMs)
|
||||
) {
|
||||
points.push({
|
||||
t: now,
|
||||
v: currentValue
|
||||
v: currentValue,
|
||||
rawV: currentRawValue
|
||||
});
|
||||
}
|
||||
|
||||
@@ -353,18 +532,41 @@ unique_bubble_graph:
|
||||
});
|
||||
};
|
||||
|
||||
const attachTooltip = (seriesList) => {
|
||||
const attachTooltipOnce = () => {
|
||||
if (cfg.tooltip === false) return;
|
||||
|
||||
host.__bubbleHistoryTooltip = tooltip;
|
||||
host.__bubbleHistoryMarker = marker;
|
||||
host.__bubbleHistoryTooltipTop = tooltipTop;
|
||||
|
||||
if (host.__bubbleHistoryTooltipAttached) return;
|
||||
|
||||
host.__bubbleHistoryTooltipAttached = true;
|
||||
|
||||
host.onmousemove = (event) => {
|
||||
const usableSeries = seriesList.filter((series) => series.points?.length);
|
||||
const usableSeries = (host.__bubbleHistorySeries || [])
|
||||
.filter((series) => series.points?.length);
|
||||
|
||||
if (!usableSeries.length) return;
|
||||
|
||||
const tooltipElement = host.__bubbleHistoryTooltip;
|
||||
const markerElement = host.__bubbleHistoryMarker;
|
||||
|
||||
if (!tooltipElement || !markerElement) return;
|
||||
|
||||
const rect = host.getBoundingClientRect();
|
||||
const xRatio = Math.min(1, Math.max(0, (event.clientX - rect.left) / rect.width));
|
||||
|
||||
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 globalMinTime =
|
||||
Number.isFinite(host.__bubbleHistoryMinTime)
|
||||
? host.__bubbleHistoryMinTime
|
||||
: Math.min(...usableSeries.map((series) => series.points[0].t));
|
||||
|
||||
const globalMaxTime =
|
||||
Number.isFinite(host.__bubbleHistoryMaxTime)
|
||||
? host.__bubbleHistoryMaxTime
|
||||
: Math.max(...usableSeries.map((series) => series.points[series.points.length - 1].t));
|
||||
|
||||
const targetTime = globalMinTime + xRatio * (globalMaxTime - globalMinTime);
|
||||
|
||||
const rows = usableSeries.map((series) => {
|
||||
@@ -374,7 +576,7 @@ unique_bubble_graph:
|
||||
return `
|
||||
<div class="bubble-history-tooltip-row">
|
||||
<span class="bubble-history-tooltip-dot" style="background:${series.color};"></span>
|
||||
<span><strong>${series.label}:</strong> ${formatValue(nearest.v, series.unit)}</span>
|
||||
<span><strong>${series.label}:</strong> ${formatTooltipValue(nearest, series)}</span>
|
||||
</div>
|
||||
`;
|
||||
}).join("");
|
||||
@@ -385,22 +587,48 @@ unique_bubble_graph:
|
||||
const x = ((targetTime - globalMinTime) / (globalMaxTime - globalMinTime || 1)) * rect.width;
|
||||
const safeX = Math.min(rect.width - 60, Math.max(60, x));
|
||||
|
||||
tooltip.innerHTML = `
|
||||
tooltipElement.innerHTML = `
|
||||
${rows}
|
||||
<div style="opacity:0.75;margin-top:3px;">${formatTime(tooltipTime)}</div>
|
||||
`;
|
||||
|
||||
tooltip.style.display = "block";
|
||||
marker.style.display = "block";
|
||||
tooltipElement.style.display = "block";
|
||||
markerElement.style.display = "block";
|
||||
|
||||
tooltip.style.left = `${safeX}px`;
|
||||
tooltip.style.top = `${tooltipTop}px`;
|
||||
marker.style.left = `${x}px`;
|
||||
tooltipElement.style.left = `${safeX}px`;
|
||||
tooltipElement.style.top = `${host.__bubbleHistoryTooltipTop}px`;
|
||||
markerElement.style.left = `${x}px`;
|
||||
};
|
||||
|
||||
host.onmouseleave = () => {
|
||||
tooltip.style.display = "none";
|
||||
marker.style.display = "none";
|
||||
const tooltipElement = host.__bubbleHistoryTooltip;
|
||||
const markerElement = host.__bubbleHistoryMarker;
|
||||
|
||||
if (tooltipElement) tooltipElement.style.display = "none";
|
||||
if (markerElement) markerElement.style.display = "none";
|
||||
};
|
||||
};
|
||||
|
||||
const getSameScaleRange = (seriesList) => {
|
||||
const allValues = seriesList.flatMap((series) =>
|
||||
(series.points || []).map((point) => point.v)
|
||||
);
|
||||
|
||||
if (!allValues.length) return null;
|
||||
|
||||
const rawMinValue = Math.min(...allValues);
|
||||
const rawMaxValue = Math.max(...allValues);
|
||||
|
||||
const rawRange = rawMaxValue - rawMinValue;
|
||||
const safeRange = rawRange || Math.max(Math.abs(rawMaxValue), 1);
|
||||
const valuePadding = safeRange * (valuePaddingPercent / 100);
|
||||
|
||||
const calculatedMinValue = rawMinValue - valuePadding;
|
||||
const calculatedMaxValue = rawMaxValue + valuePadding;
|
||||
|
||||
return {
|
||||
minValue: Number.isFinite(sameScaleMin) ? sameScaleMin : calculatedMinValue,
|
||||
maxValue: Number.isFinite(sameScaleMax) ? sameScaleMax : calculatedMaxValue
|
||||
};
|
||||
};
|
||||
|
||||
@@ -415,8 +643,16 @@ unique_bubble_graph:
|
||||
|
||||
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 sameScaleRange = sameScale ? getSameScaleRange(usableSeries) : null;
|
||||
|
||||
const buildPath = (series) => {
|
||||
let minValue;
|
||||
let maxValue;
|
||||
|
||||
if (sameScaleRange) {
|
||||
minValue = sameScaleRange.minValue;
|
||||
maxValue = sameScaleRange.maxValue;
|
||||
} else {
|
||||
const rawMinValue = Math.min(...series.points.map((point) => point.v));
|
||||
const rawMaxValue = Math.max(...series.points.map((point) => point.v));
|
||||
|
||||
@@ -424,8 +660,9 @@ unique_bubble_graph:
|
||||
const safeRange = rawRange || Math.max(Math.abs(rawMaxValue), 1);
|
||||
const valuePadding = safeRange * (valuePaddingPercent / 100);
|
||||
|
||||
const minValue = rawMinValue - valuePadding;
|
||||
const maxValue = rawMaxValue + valuePadding;
|
||||
minValue = rawMinValue - valuePadding;
|
||||
maxValue = rawMaxValue + valuePadding;
|
||||
}
|
||||
|
||||
const timeRange = globalMaxTime - globalMinTime || 1;
|
||||
const valueRange = maxValue - minValue || 1;
|
||||
@@ -477,7 +714,10 @@ unique_bubble_graph:
|
||||
</svg>
|
||||
`;
|
||||
|
||||
attachTooltip(usableSeries);
|
||||
host.__bubbleHistorySeries = usableSeries;
|
||||
host.__bubbleHistoryMinTime = globalMinTime;
|
||||
host.__bubbleHistoryMaxTime = globalMaxTime;
|
||||
attachTooltipOnce();
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -500,13 +740,41 @@ unique_bubble_graph:
|
||||
return "";
|
||||
}
|
||||
|
||||
const dataCacheParts = validGraphs.map((graph) => graph.entity).join("|");
|
||||
const applyCurrentGraphSettingsToSeries = (seriesList) => {
|
||||
if (!Array.isArray(seriesList)) return [];
|
||||
|
||||
return seriesList.map((cachedSeries) => {
|
||||
const currentGraph = validGraphs.find((graph) => graph.entity === cachedSeries.entity);
|
||||
|
||||
if (!currentGraph) return cachedSeries;
|
||||
|
||||
return {
|
||||
...cachedSeries,
|
||||
label: currentGraph.label,
|
||||
color: currentGraph.color,
|
||||
lineWidth: currentGraph.lineWidth,
|
||||
lineOpacity: currentGraph.lineOpacity,
|
||||
fillOpacity: currentGraph.fillOpacity,
|
||||
rawUnit: currentGraph.rawUnit,
|
||||
unit: currentGraph.unit,
|
||||
valueMode: currentGraph.valueMode,
|
||||
maxValue: currentGraph.maxValue
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const dataCacheParts = validGraphs
|
||||
.map((graph) => `${graph.entity}:${graph.valueMode}:${graph.maxValue ?? ""}`)
|
||||
.join("|");
|
||||
|
||||
const cacheKey = [
|
||||
"unique-bubble-graph-data-v260",
|
||||
"unique-bubble-graph-data-v285",
|
||||
dataCacheParts,
|
||||
hoursToShow,
|
||||
maxPoints
|
||||
maxPoints,
|
||||
sameScale ? "same-scale" : "individual-scale",
|
||||
sameScaleMin ?? "auto-min",
|
||||
sameScaleMax ?? "auto-max"
|
||||
].join("::");
|
||||
|
||||
const storageKey = `unique-bubble-graph-data-cache:${cacheKey}`;
|
||||
@@ -558,6 +826,11 @@ unique_bubble_graph:
|
||||
readPersistentCache();
|
||||
|
||||
if (cache?.seriesList) {
|
||||
cache = {
|
||||
...cache,
|
||||
seriesList: applyCurrentGraphSettingsToSeries(cache.seriesList)
|
||||
};
|
||||
|
||||
renderGraph(cache.seriesList);
|
||||
|
||||
window.__uniqueBubbleGraphDataCache[cacheKey] = {
|
||||
@@ -600,26 +873,23 @@ unique_bubble_graph:
|
||||
const states = Array.isArray(history) ? history.flat() : [];
|
||||
|
||||
let points = states
|
||||
.map((state) => ({
|
||||
.map((state) => {
|
||||
const rawValue = Number(state.s ?? state.state);
|
||||
|
||||
return {
|
||||
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))
|
||||
v: transformValueForGraph(rawValue, graph),
|
||||
rawV: rawValue
|
||||
};
|
||||
})
|
||||
.filter((point) =>
|
||||
Number.isFinite(point.t) &&
|
||||
Number.isFinite(point.v) &&
|
||||
Number.isFinite(point.rawV)
|
||||
)
|
||||
.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);
|
||||
}
|
||||
}
|
||||
points = downsampleMinMax(points, maxPoints);
|
||||
|
||||
return {
|
||||
...graph,
|
||||
@@ -672,8 +942,14 @@ unique_bubble_graph:
|
||||
return "";
|
||||
})()}
|
||||
editor:
|
||||
- type: expandable
|
||||
name: general_settings
|
||||
title: Allgemein
|
||||
icon: mdi:cog-outline
|
||||
expanded: true
|
||||
schema:
|
||||
- name: hours_to_show
|
||||
label: Allgemein · Stunden anzeigen
|
||||
label: Stunden anzeigen
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
@@ -681,7 +957,7 @@ unique_bubble_graph:
|
||||
step: 1
|
||||
mode: box
|
||||
- name: refresh_minutes
|
||||
label: Allgemein · Aktualisierung Minuten
|
||||
label: Aktualisierung Minuten
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
@@ -689,11 +965,11 @@ unique_bubble_graph:
|
||||
step: 1
|
||||
mode: box
|
||||
- name: persistent_cache
|
||||
label: Allgemein · Cache nach F5 behalten
|
||||
label: Cache nach F5 behalten
|
||||
selector:
|
||||
boolean: null
|
||||
- name: max_cache_age_hours
|
||||
label: Allgemein · Max. Cache-Alter Stunden
|
||||
label: Max. Cache-Alter Stunden
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
@@ -701,25 +977,58 @@ unique_bubble_graph:
|
||||
step: 1
|
||||
mode: box
|
||||
- name: show_loading
|
||||
label: Allgemein · Lade-Kringel anzeigen
|
||||
label: Lade-Kringel anzeigen
|
||||
selector:
|
||||
boolean: null
|
||||
|
||||
- type: expandable
|
||||
name: scale_settings
|
||||
title: Skalierung & Prozent-Umrechnung
|
||||
icon: mdi:chart-line-variant
|
||||
expanded: true
|
||||
schema:
|
||||
- name: same_scale
|
||||
label: Gemeinsame Skalierung für alle Graphen
|
||||
selector:
|
||||
boolean: null
|
||||
- name: same_scale_min
|
||||
label: Gemeinsame Skala Minimum
|
||||
selector:
|
||||
number:
|
||||
min: -100000
|
||||
max: 100000
|
||||
step: 1
|
||||
mode: box
|
||||
- name: same_scale_max
|
||||
label: Gemeinsame Skala Maximum
|
||||
selector:
|
||||
number:
|
||||
min: -100000
|
||||
max: 100000
|
||||
step: 1
|
||||
mode: box
|
||||
|
||||
- type: expandable
|
||||
name: graph_1_settings
|
||||
title: ① Graph
|
||||
icon: mdi:numeric-1-circle-outline
|
||||
expanded: true
|
||||
schema:
|
||||
- name: graph_1_entity
|
||||
label: ① Graph · Sensor
|
||||
label: Sensor
|
||||
selector:
|
||||
entity:
|
||||
domain: sensor
|
||||
- name: graph_1_label
|
||||
label: ① Graph · Label
|
||||
label: Label
|
||||
selector:
|
||||
text: null
|
||||
- name: graph_1_color
|
||||
label: ① Graph · Farbe
|
||||
label: Farbe
|
||||
selector:
|
||||
text: null
|
||||
color_rgb: null
|
||||
- name: graph_1_line_width
|
||||
label: ① Graph · Linien Dicke
|
||||
label: Linien Dicke
|
||||
selector:
|
||||
number:
|
||||
min: 0.5
|
||||
@@ -727,29 +1036,52 @@ unique_bubble_graph:
|
||||
step: 0.1
|
||||
mode: box
|
||||
- name: graph_1_fill_opacity
|
||||
label: ① Graph · Fläche
|
||||
label: Fläche
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 1
|
||||
step: 0.05
|
||||
mode: slider
|
||||
- name: graph_1_value_mode
|
||||
label: Wert-Modus
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- label: Originalwert
|
||||
value: default
|
||||
- label: Prozent von Maximalwert
|
||||
value: percent_of_max
|
||||
- name: graph_1_max_value
|
||||
label: Maximalwert für Prozent
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 100000
|
||||
step: 1
|
||||
mode: box
|
||||
|
||||
- type: expandable
|
||||
name: graph_2_settings
|
||||
title: ② Graph
|
||||
icon: mdi:numeric-2-circle-outline
|
||||
expanded: false
|
||||
schema:
|
||||
- name: graph_2_entity
|
||||
label: ② Graph · Sensor
|
||||
label: Sensor
|
||||
selector:
|
||||
entity:
|
||||
domain: sensor
|
||||
- name: graph_2_label
|
||||
label: ② Graph · Label
|
||||
label: Label
|
||||
selector:
|
||||
text: null
|
||||
- name: graph_2_color
|
||||
label: ② Graph · Farbe
|
||||
label: Farbe
|
||||
selector:
|
||||
text: null
|
||||
color_rgb: null
|
||||
- name: graph_2_line_width
|
||||
label: ② Graph · Linien Dicke
|
||||
label: Linien Dicke
|
||||
selector:
|
||||
number:
|
||||
min: 0.5
|
||||
@@ -757,29 +1089,52 @@ unique_bubble_graph:
|
||||
step: 0.1
|
||||
mode: box
|
||||
- name: graph_2_fill_opacity
|
||||
label: ② Graph · Fläche
|
||||
label: Fläche
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 1
|
||||
step: 0.05
|
||||
mode: slider
|
||||
- name: graph_2_value_mode
|
||||
label: Wert-Modus
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- label: Originalwert
|
||||
value: default
|
||||
- label: Prozent von Maximalwert
|
||||
value: percent_of_max
|
||||
- name: graph_2_max_value
|
||||
label: Maximalwert für Prozent
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 100000
|
||||
step: 1
|
||||
mode: box
|
||||
|
||||
- type: expandable
|
||||
name: graph_3_settings
|
||||
title: ③ Graph
|
||||
icon: mdi:numeric-3-circle-outline
|
||||
expanded: false
|
||||
schema:
|
||||
- name: graph_3_entity
|
||||
label: ③ Graph · Sensor
|
||||
label: Sensor
|
||||
selector:
|
||||
entity:
|
||||
domain: sensor
|
||||
- name: graph_3_label
|
||||
label: ③ Graph · Label
|
||||
label: Label
|
||||
selector:
|
||||
text: null
|
||||
- name: graph_3_color
|
||||
label: ③ Graph · Farbe
|
||||
label: Farbe
|
||||
selector:
|
||||
text: null
|
||||
color_rgb: null
|
||||
- name: graph_3_line_width
|
||||
label: ③ Graph · Linien Dicke
|
||||
label: Linien Dicke
|
||||
selector:
|
||||
number:
|
||||
min: 0.5
|
||||
@@ -787,69 +1142,43 @@ unique_bubble_graph:
|
||||
step: 0.1
|
||||
mode: box
|
||||
- name: graph_3_fill_opacity
|
||||
label: ③ Graph · Fläche
|
||||
label: Fläche
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 1
|
||||
step: 0.05
|
||||
mode: slider
|
||||
|
||||
- name: line_width
|
||||
label: Anzeige · Standard Linien Dicke
|
||||
- name: graph_3_value_mode
|
||||
label: Wert-Modus
|
||||
selector:
|
||||
number:
|
||||
min: 0.5
|
||||
max: 10
|
||||
step: 0.1
|
||||
mode: box
|
||||
- name: line_opacity
|
||||
label: Anzeige · Linien Sichtbarkeit
|
||||
select:
|
||||
options:
|
||||
- label: Originalwert
|
||||
value: default
|
||||
- label: Prozent von Maximalwert
|
||||
value: percent_of_max
|
||||
- name: graph_3_max_value
|
||||
label: Maximalwert für Prozent
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 1
|
||||
step: 0.05
|
||||
mode: slider
|
||||
- name: fill_opacity
|
||||
label: Anzeige · Standard Fläche
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 1
|
||||
step: 0.05
|
||||
mode: slider
|
||||
- name: value_padding_percent
|
||||
label: Anzeige · Abstand oben/unten Prozent
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 30
|
||||
step: 1
|
||||
mode: box
|
||||
- name: padding_y
|
||||
label: Anzeige · Graph Abstand oben/unten
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 40
|
||||
step: 1
|
||||
mode: box
|
||||
- name: border_radius
|
||||
label: Anzeige · Eckenradius
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 60
|
||||
max: 100000
|
||||
step: 1
|
||||
mode: box
|
||||
|
||||
- type: expandable
|
||||
name: advanced_settings
|
||||
title: Erweitert
|
||||
icon: mdi:tune-variant
|
||||
expanded: false
|
||||
schema:
|
||||
- name: extend_to_now
|
||||
label: Erweitert · Linie bis jetzt verlängern
|
||||
label: Linie bis jetzt verlängern
|
||||
selector:
|
||||
boolean: null
|
||||
- name: extend_threshold_minutes
|
||||
label: Erweitert · Verlängern nach Minuten
|
||||
label: Verlängern nach Minuten
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
@@ -857,7 +1186,7 @@ unique_bubble_graph:
|
||||
step: 1
|
||||
mode: box
|
||||
- name: max_points
|
||||
label: Erweitert · Max. Datenpunkte
|
||||
label: Max. Datenpunkte
|
||||
selector:
|
||||
number:
|
||||
min: 20
|
||||
@@ -865,11 +1194,11 @@ unique_bubble_graph:
|
||||
step: 10
|
||||
mode: box
|
||||
- name: tooltip
|
||||
label: Erweitert · Tooltip anzeigen
|
||||
label: Tooltip anzeigen
|
||||
selector:
|
||||
boolean: null
|
||||
- name: tooltip_top
|
||||
label: Erweitert · Tooltip Position oben
|
||||
label: Tooltip Position oben
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
|
||||
Reference in New Issue
Block a user