unique_bubble_graph.yaml aktualisiert
This commit is contained in:
@@ -1,10 +1,10 @@
|
|||||||
unique_bubble_graph:
|
unique_bubble_graph:
|
||||||
name: Unique Bubble Multi History Background Graph
|
name: Unique Bubble Multi History Background Graph
|
||||||
version: 2.6.0
|
version: 2.8.6
|
||||||
creator: Torsten
|
creator: Torsten
|
||||||
supported:
|
supported:
|
||||||
- button
|
- 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: |
|
code: |
|
||||||
ha-card {
|
ha-card {
|
||||||
position: relative !important;
|
position: relative !important;
|
||||||
@@ -134,11 +134,34 @@ unique_bubble_graph:
|
|||||||
}
|
}
|
||||||
|
|
||||||
${(() => {
|
${(() => {
|
||||||
const cfg =
|
const rawCfg =
|
||||||
this.config.unique_bubble_graph ||
|
this.config.unique_bubble_graph ||
|
||||||
this.config.history_background_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 hoursToShow = Number(cfg.hours_to_show ?? 24);
|
||||||
const refreshMinutes = Number(cfg.refresh_minutes ?? 10);
|
const refreshMinutes = Number(cfg.refresh_minutes ?? 10);
|
||||||
const maxPoints = Number(cfg.max_points ?? 120);
|
const maxPoints = Number(cfg.max_points ?? 120);
|
||||||
@@ -153,80 +176,172 @@ unique_bubble_graph:
|
|||||||
const extendToNow = cfg.extend_to_now !== false;
|
const extendToNow = cfg.extend_to_now !== false;
|
||||||
const extendThresholdMinutes = Number(cfg.extend_threshold_minutes ?? 2);
|
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 defaultLineWidth = Number(cfg.line_width ?? 2.5);
|
||||||
const defaultLineOpacity = Number(cfg.line_opacity ?? 0.75);
|
const defaultLineOpacity = Number(cfg.line_opacity ?? 0.75);
|
||||||
const defaultFillOpacity = Number(cfg.fill_opacity ?? 0.08);
|
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 = [
|
const defaultColors = [
|
||||||
cfg.graph_1_color || cfg.line_color || "var(--primary-color)",
|
"var(--primary-color)",
|
||||||
cfg.graph_2_color || "#2196f3",
|
"#2196f3",
|
||||||
cfg.graph_3_color || "#4caf50"
|
"#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 = () => {
|
const buildGraphs = () => {
|
||||||
if (Array.isArray(cfg.graphs) && cfg.graphs.length) {
|
if (Array.isArray(cfg.graphs) && cfg.graphs.length) {
|
||||||
return cfg.graphs
|
return cfg.graphs
|
||||||
.slice(0, 3)
|
.slice(0, 3)
|
||||||
.map((graph, index) => ({
|
.map((graph, index) => {
|
||||||
|
const scaleConfig = getGraphScaleConfig(graph, index);
|
||||||
|
|
||||||
|
return {
|
||||||
entity: graph.entity,
|
entity: graph.entity,
|
||||||
label:
|
label:
|
||||||
graph.label ||
|
graph.label ||
|
||||||
hass.states?.[graph.entity]?.attributes?.friendly_name ||
|
hass.states?.[graph.entity]?.attributes?.friendly_name ||
|
||||||
graph.entity ||
|
graph.entity ||
|
||||||
`Graph ${index + 1}`,
|
`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),
|
lineWidth: Number(graph.line_width ?? defaultLineWidth),
|
||||||
lineOpacity: Number(graph.line_opacity ?? defaultLineOpacity),
|
lineOpacity: Number(graph.line_opacity ?? defaultLineOpacity),
|
||||||
fillOpacity: Number(graph.fill_opacity ?? defaultFillOpacity),
|
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);
|
.filter((graph) => graph.entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
const graph1Entity = cfg.graph_1_entity || cfg.entity || this.config.entity || "sensor.sma_tripower_x_pv_power";
|
const graph1Entity = getGraphSetting(0, "entity");
|
||||||
const graph2Entity = cfg.graph_2_entity;
|
const graph2Entity = getGraphSetting(1, "entity");
|
||||||
const graph3Entity = cfg.graph_3_entity;
|
const graph3Entity = getGraphSetting(2, "entity");
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
entity: graph1Entity,
|
entity: graph1Entity,
|
||||||
label:
|
label:
|
||||||
cfg.graph_1_label ||
|
getGraphSetting(0, "label") ||
|
||||||
cfg.label ||
|
cfg.label ||
|
||||||
hass.states?.[graph1Entity]?.attributes?.friendly_name ||
|
hass.states?.[graph1Entity]?.attributes?.friendly_name ||
|
||||||
graph1Entity ||
|
graph1Entity ||
|
||||||
"Graph 1",
|
"Graph 1",
|
||||||
color: cfg.graph_1_color || cfg.line_color || defaultColors[0],
|
color: normalizeColor(getGraphSetting(0, "color") || cfg.line_color, defaultColors[0]),
|
||||||
lineWidth: Number(cfg.graph_1_line_width ?? defaultLineWidth),
|
lineWidth: Number(getGraphSetting(0, "line_width", defaultLineWidth)),
|
||||||
lineOpacity: Number(cfg.graph_1_line_opacity ?? defaultLineOpacity),
|
lineOpacity: Number(getGraphSetting(0, "line_opacity", defaultLineOpacity)),
|
||||||
fillOpacity: Number(cfg.graph_1_fill_opacity ?? defaultFillOpacity),
|
fillOpacity: Number(getGraphSetting(0, "fill_opacity", defaultFillOpacity)),
|
||||||
unit: hass.states?.[graph1Entity]?.attributes?.unit_of_measurement || ""
|
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,
|
entity: graph2Entity,
|
||||||
label:
|
label:
|
||||||
cfg.graph_2_label ||
|
getGraphSetting(1, "label") ||
|
||||||
hass.states?.[graph2Entity]?.attributes?.friendly_name ||
|
hass.states?.[graph2Entity]?.attributes?.friendly_name ||
|
||||||
graph2Entity ||
|
graph2Entity ||
|
||||||
"Graph 2",
|
"Graph 2",
|
||||||
color: cfg.graph_2_color || defaultColors[1],
|
color: normalizeColor(getGraphSetting(1, "color"), defaultColors[1]),
|
||||||
lineWidth: Number(cfg.graph_2_line_width ?? defaultLineWidth),
|
lineWidth: Number(getGraphSetting(1, "line_width", defaultLineWidth)),
|
||||||
lineOpacity: Number(cfg.graph_2_line_opacity ?? defaultLineOpacity),
|
lineOpacity: Number(getGraphSetting(1, "line_opacity", defaultLineOpacity)),
|
||||||
fillOpacity: Number(cfg.graph_2_fill_opacity ?? defaultFillOpacity),
|
fillOpacity: Number(getGraphSetting(1, "fill_opacity", defaultFillOpacity)),
|
||||||
unit: hass.states?.[graph2Entity]?.attributes?.unit_of_measurement || ""
|
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,
|
entity: graph3Entity,
|
||||||
label:
|
label:
|
||||||
cfg.graph_3_label ||
|
getGraphSetting(2, "label") ||
|
||||||
hass.states?.[graph3Entity]?.attributes?.friendly_name ||
|
hass.states?.[graph3Entity]?.attributes?.friendly_name ||
|
||||||
graph3Entity ||
|
graph3Entity ||
|
||||||
"Graph 3",
|
"Graph 3",
|
||||||
color: cfg.graph_3_color || defaultColors[2],
|
color: normalizeColor(getGraphSetting(2, "color"), defaultColors[2]),
|
||||||
lineWidth: Number(cfg.graph_3_line_width ?? defaultLineWidth),
|
lineWidth: Number(getGraphSetting(2, "line_width", defaultLineWidth)),
|
||||||
lineOpacity: Number(cfg.graph_3_line_opacity ?? defaultLineOpacity),
|
lineOpacity: Number(getGraphSetting(2, "line_opacity", defaultLineOpacity)),
|
||||||
fillOpacity: Number(cfg.graph_3_fill_opacity ?? defaultFillOpacity),
|
fillOpacity: Number(getGraphSetting(2, "fill_opacity", defaultFillOpacity)),
|
||||||
unit: hass.states?.[graph3Entity]?.attributes?.unit_of_measurement || ""
|
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);
|
].filter((graph) => graph.entity);
|
||||||
};
|
};
|
||||||
@@ -299,6 +414,31 @@ unique_bubble_graph:
|
|||||||
return `${value.toFixed(1)} ${unit}`.trim();
|
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) => {
|
const formatTime = (timestamp) => {
|
||||||
return new Date(timestamp).toLocaleString("de-DE", {
|
return new Date(timestamp).toLocaleString("de-DE", {
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
@@ -325,6 +465,42 @@ unique_bubble_graph:
|
|||||||
return nearest;
|
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) => {
|
const extendSeriesToNow = (seriesList) => {
|
||||||
if (!extendToNow) return seriesList;
|
if (!extendToNow) return seriesList;
|
||||||
|
|
||||||
@@ -334,15 +510,18 @@ unique_bubble_graph:
|
|||||||
return seriesList.map((series) => {
|
return seriesList.map((series) => {
|
||||||
const points = [...(series.points || [])];
|
const points = [...(series.points || [])];
|
||||||
const lastPoint = points[points.length - 1];
|
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 (
|
if (
|
||||||
|
Number.isFinite(currentRawValue) &&
|
||||||
Number.isFinite(currentValue) &&
|
Number.isFinite(currentValue) &&
|
||||||
(!lastPoint || now - lastPoint.t > thresholdMs)
|
(!lastPoint || now - lastPoint.t > thresholdMs)
|
||||||
) {
|
) {
|
||||||
points.push({
|
points.push({
|
||||||
t: now,
|
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;
|
if (cfg.tooltip === false) return;
|
||||||
|
|
||||||
|
host.__bubbleHistoryTooltip = tooltip;
|
||||||
|
host.__bubbleHistoryMarker = marker;
|
||||||
|
host.__bubbleHistoryTooltipTop = tooltipTop;
|
||||||
|
|
||||||
|
if (host.__bubbleHistoryTooltipAttached) return;
|
||||||
|
|
||||||
|
host.__bubbleHistoryTooltipAttached = true;
|
||||||
|
|
||||||
host.onmousemove = (event) => {
|
host.onmousemove = (event) => {
|
||||||
const usableSeries = seriesList.filter((series) => series.points?.length);
|
const usableSeries = (host.__bubbleHistorySeries || [])
|
||||||
|
.filter((series) => series.points?.length);
|
||||||
|
|
||||||
if (!usableSeries.length) return;
|
if (!usableSeries.length) return;
|
||||||
|
|
||||||
|
const tooltipElement = host.__bubbleHistoryTooltip;
|
||||||
|
const markerElement = host.__bubbleHistoryMarker;
|
||||||
|
|
||||||
|
if (!tooltipElement || !markerElement) return;
|
||||||
|
|
||||||
const rect = host.getBoundingClientRect();
|
const rect = host.getBoundingClientRect();
|
||||||
const xRatio = Math.min(1, Math.max(0, (event.clientX - rect.left) / rect.width));
|
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 globalMinTime =
|
||||||
const globalMaxTime = Math.max(...usableSeries.map((series) => series.points[series.points.length - 1].t));
|
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 targetTime = globalMinTime + xRatio * (globalMaxTime - globalMinTime);
|
||||||
|
|
||||||
const rows = usableSeries.map((series) => {
|
const rows = usableSeries.map((series) => {
|
||||||
@@ -374,7 +576,7 @@ unique_bubble_graph:
|
|||||||
return `
|
return `
|
||||||
<div class="bubble-history-tooltip-row">
|
<div class="bubble-history-tooltip-row">
|
||||||
<span class="bubble-history-tooltip-dot" style="background:${series.color};"></span>
|
<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>
|
</div>
|
||||||
`;
|
`;
|
||||||
}).join("");
|
}).join("");
|
||||||
@@ -385,22 +587,48 @@ unique_bubble_graph:
|
|||||||
const x = ((targetTime - globalMinTime) / (globalMaxTime - globalMinTime || 1)) * rect.width;
|
const x = ((targetTime - globalMinTime) / (globalMaxTime - globalMinTime || 1)) * rect.width;
|
||||||
const safeX = Math.min(rect.width - 60, Math.max(60, x));
|
const safeX = Math.min(rect.width - 60, Math.max(60, x));
|
||||||
|
|
||||||
tooltip.innerHTML = `
|
tooltipElement.innerHTML = `
|
||||||
${rows}
|
${rows}
|
||||||
<div style="opacity:0.75;margin-top:3px;">${formatTime(tooltipTime)}</div>
|
<div style="opacity:0.75;margin-top:3px;">${formatTime(tooltipTime)}</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
tooltip.style.display = "block";
|
tooltipElement.style.display = "block";
|
||||||
marker.style.display = "block";
|
markerElement.style.display = "block";
|
||||||
|
|
||||||
tooltip.style.left = `${safeX}px`;
|
tooltipElement.style.left = `${safeX}px`;
|
||||||
tooltip.style.top = `${tooltipTop}px`;
|
tooltipElement.style.top = `${host.__bubbleHistoryTooltipTop}px`;
|
||||||
marker.style.left = `${x}px`;
|
markerElement.style.left = `${x}px`;
|
||||||
};
|
};
|
||||||
|
|
||||||
host.onmouseleave = () => {
|
host.onmouseleave = () => {
|
||||||
tooltip.style.display = "none";
|
const tooltipElement = host.__bubbleHistoryTooltip;
|
||||||
marker.style.display = "none";
|
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 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 globalMaxTime = Math.max(...usableSeries.map((series) => series.points[series.points.length - 1].t));
|
||||||
|
const sameScaleRange = sameScale ? getSameScaleRange(usableSeries) : null;
|
||||||
|
|
||||||
const buildPath = (series) => {
|
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 rawMinValue = Math.min(...series.points.map((point) => point.v));
|
||||||
const rawMaxValue = Math.max(...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 safeRange = rawRange || Math.max(Math.abs(rawMaxValue), 1);
|
||||||
const valuePadding = safeRange * (valuePaddingPercent / 100);
|
const valuePadding = safeRange * (valuePaddingPercent / 100);
|
||||||
|
|
||||||
const minValue = rawMinValue - valuePadding;
|
minValue = rawMinValue - valuePadding;
|
||||||
const maxValue = rawMaxValue + valuePadding;
|
maxValue = rawMaxValue + valuePadding;
|
||||||
|
}
|
||||||
|
|
||||||
const timeRange = globalMaxTime - globalMinTime || 1;
|
const timeRange = globalMaxTime - globalMinTime || 1;
|
||||||
const valueRange = maxValue - minValue || 1;
|
const valueRange = maxValue - minValue || 1;
|
||||||
@@ -477,7 +714,10 @@ unique_bubble_graph:
|
|||||||
</svg>
|
</svg>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
attachTooltip(usableSeries);
|
host.__bubbleHistorySeries = usableSeries;
|
||||||
|
host.__bubbleHistoryMinTime = globalMinTime;
|
||||||
|
host.__bubbleHistoryMaxTime = globalMaxTime;
|
||||||
|
attachTooltipOnce();
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -500,13 +740,41 @@ unique_bubble_graph:
|
|||||||
return "";
|
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 = [
|
const cacheKey = [
|
||||||
"unique-bubble-graph-data-v260",
|
"unique-bubble-graph-data-v285",
|
||||||
dataCacheParts,
|
dataCacheParts,
|
||||||
hoursToShow,
|
hoursToShow,
|
||||||
maxPoints
|
maxPoints,
|
||||||
|
sameScale ? "same-scale" : "individual-scale",
|
||||||
|
sameScaleMin ?? "auto-min",
|
||||||
|
sameScaleMax ?? "auto-max"
|
||||||
].join("::");
|
].join("::");
|
||||||
|
|
||||||
const storageKey = `unique-bubble-graph-data-cache:${cacheKey}`;
|
const storageKey = `unique-bubble-graph-data-cache:${cacheKey}`;
|
||||||
@@ -558,6 +826,11 @@ unique_bubble_graph:
|
|||||||
readPersistentCache();
|
readPersistentCache();
|
||||||
|
|
||||||
if (cache?.seriesList) {
|
if (cache?.seriesList) {
|
||||||
|
cache = {
|
||||||
|
...cache,
|
||||||
|
seriesList: applyCurrentGraphSettingsToSeries(cache.seriesList)
|
||||||
|
};
|
||||||
|
|
||||||
renderGraph(cache.seriesList);
|
renderGraph(cache.seriesList);
|
||||||
|
|
||||||
window.__uniqueBubbleGraphDataCache[cacheKey] = {
|
window.__uniqueBubbleGraphDataCache[cacheKey] = {
|
||||||
@@ -600,26 +873,23 @@ unique_bubble_graph:
|
|||||||
const states = Array.isArray(history) ? history.flat() : [];
|
const states = Array.isArray(history) ? history.flat() : [];
|
||||||
|
|
||||||
let points = states
|
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(),
|
t: new Date(state.lu || state.lc || state.last_updated || state.last_changed).getTime(),
|
||||||
v: Number(state.s ?? state.state)
|
v: transformValueForGraph(rawValue, graph),
|
||||||
}))
|
rawV: rawValue
|
||||||
.filter((point) => Number.isFinite(point.t) && Number.isFinite(point.v))
|
};
|
||||||
|
})
|
||||||
|
.filter((point) =>
|
||||||
|
Number.isFinite(point.t) &&
|
||||||
|
Number.isFinite(point.v) &&
|
||||||
|
Number.isFinite(point.rawV)
|
||||||
|
)
|
||||||
.sort((a, b) => a.t - b.t);
|
.sort((a, b) => a.t - b.t);
|
||||||
|
|
||||||
if (points.length > maxPoints) {
|
points = downsampleMinMax(points, 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 {
|
return {
|
||||||
...graph,
|
...graph,
|
||||||
@@ -672,8 +942,14 @@ unique_bubble_graph:
|
|||||||
return "";
|
return "";
|
||||||
})()}
|
})()}
|
||||||
editor:
|
editor:
|
||||||
|
- type: expandable
|
||||||
|
name: general_settings
|
||||||
|
title: Allgemein
|
||||||
|
icon: mdi:cog-outline
|
||||||
|
expanded: true
|
||||||
|
schema:
|
||||||
- name: hours_to_show
|
- name: hours_to_show
|
||||||
label: Allgemein · Stunden anzeigen
|
label: Stunden anzeigen
|
||||||
selector:
|
selector:
|
||||||
number:
|
number:
|
||||||
min: 1
|
min: 1
|
||||||
@@ -681,7 +957,7 @@ unique_bubble_graph:
|
|||||||
step: 1
|
step: 1
|
||||||
mode: box
|
mode: box
|
||||||
- name: refresh_minutes
|
- name: refresh_minutes
|
||||||
label: Allgemein · Aktualisierung Minuten
|
label: Aktualisierung Minuten
|
||||||
selector:
|
selector:
|
||||||
number:
|
number:
|
||||||
min: 1
|
min: 1
|
||||||
@@ -689,11 +965,11 @@ unique_bubble_graph:
|
|||||||
step: 1
|
step: 1
|
||||||
mode: box
|
mode: box
|
||||||
- name: persistent_cache
|
- name: persistent_cache
|
||||||
label: Allgemein · Cache nach F5 behalten
|
label: Cache nach F5 behalten
|
||||||
selector:
|
selector:
|
||||||
boolean: null
|
boolean: null
|
||||||
- name: max_cache_age_hours
|
- name: max_cache_age_hours
|
||||||
label: Allgemein · Max. Cache-Alter Stunden
|
label: Max. Cache-Alter Stunden
|
||||||
selector:
|
selector:
|
||||||
number:
|
number:
|
||||||
min: 1
|
min: 1
|
||||||
@@ -701,25 +977,58 @@ unique_bubble_graph:
|
|||||||
step: 1
|
step: 1
|
||||||
mode: box
|
mode: box
|
||||||
- name: show_loading
|
- name: show_loading
|
||||||
label: Allgemein · Lade-Kringel anzeigen
|
label: Lade-Kringel anzeigen
|
||||||
selector:
|
selector:
|
||||||
boolean: null
|
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
|
- name: graph_1_entity
|
||||||
label: ① Graph · Sensor
|
label: Sensor
|
||||||
selector:
|
selector:
|
||||||
entity:
|
entity:
|
||||||
domain: sensor
|
domain: sensor
|
||||||
- name: graph_1_label
|
- name: graph_1_label
|
||||||
label: ① Graph · Label
|
label: Label
|
||||||
selector:
|
selector:
|
||||||
text: null
|
text: null
|
||||||
- name: graph_1_color
|
- name: graph_1_color
|
||||||
label: ① Graph · Farbe
|
label: Farbe
|
||||||
selector:
|
selector:
|
||||||
text: null
|
color_rgb: null
|
||||||
- name: graph_1_line_width
|
- name: graph_1_line_width
|
||||||
label: ① Graph · Linien Dicke
|
label: Linien Dicke
|
||||||
selector:
|
selector:
|
||||||
number:
|
number:
|
||||||
min: 0.5
|
min: 0.5
|
||||||
@@ -727,29 +1036,52 @@ unique_bubble_graph:
|
|||||||
step: 0.1
|
step: 0.1
|
||||||
mode: box
|
mode: box
|
||||||
- name: graph_1_fill_opacity
|
- name: graph_1_fill_opacity
|
||||||
label: ① Graph · Fläche
|
label: Fläche
|
||||||
selector:
|
selector:
|
||||||
number:
|
number:
|
||||||
min: 0
|
min: 0
|
||||||
max: 1
|
max: 1
|
||||||
step: 0.05
|
step: 0.05
|
||||||
mode: slider
|
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
|
- name: graph_2_entity
|
||||||
label: ② Graph · Sensor
|
label: Sensor
|
||||||
selector:
|
selector:
|
||||||
entity:
|
entity:
|
||||||
domain: sensor
|
domain: sensor
|
||||||
- name: graph_2_label
|
- name: graph_2_label
|
||||||
label: ② Graph · Label
|
label: Label
|
||||||
selector:
|
selector:
|
||||||
text: null
|
text: null
|
||||||
- name: graph_2_color
|
- name: graph_2_color
|
||||||
label: ② Graph · Farbe
|
label: Farbe
|
||||||
selector:
|
selector:
|
||||||
text: null
|
color_rgb: null
|
||||||
- name: graph_2_line_width
|
- name: graph_2_line_width
|
||||||
label: ② Graph · Linien Dicke
|
label: Linien Dicke
|
||||||
selector:
|
selector:
|
||||||
number:
|
number:
|
||||||
min: 0.5
|
min: 0.5
|
||||||
@@ -757,29 +1089,52 @@ unique_bubble_graph:
|
|||||||
step: 0.1
|
step: 0.1
|
||||||
mode: box
|
mode: box
|
||||||
- name: graph_2_fill_opacity
|
- name: graph_2_fill_opacity
|
||||||
label: ② Graph · Fläche
|
label: Fläche
|
||||||
selector:
|
selector:
|
||||||
number:
|
number:
|
||||||
min: 0
|
min: 0
|
||||||
max: 1
|
max: 1
|
||||||
step: 0.05
|
step: 0.05
|
||||||
mode: slider
|
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
|
- name: graph_3_entity
|
||||||
label: ③ Graph · Sensor
|
label: Sensor
|
||||||
selector:
|
selector:
|
||||||
entity:
|
entity:
|
||||||
domain: sensor
|
domain: sensor
|
||||||
- name: graph_3_label
|
- name: graph_3_label
|
||||||
label: ③ Graph · Label
|
label: Label
|
||||||
selector:
|
selector:
|
||||||
text: null
|
text: null
|
||||||
- name: graph_3_color
|
- name: graph_3_color
|
||||||
label: ③ Graph · Farbe
|
label: Farbe
|
||||||
selector:
|
selector:
|
||||||
text: null
|
color_rgb: null
|
||||||
- name: graph_3_line_width
|
- name: graph_3_line_width
|
||||||
label: ③ Graph · Linien Dicke
|
label: Linien Dicke
|
||||||
selector:
|
selector:
|
||||||
number:
|
number:
|
||||||
min: 0.5
|
min: 0.5
|
||||||
@@ -787,69 +1142,43 @@ unique_bubble_graph:
|
|||||||
step: 0.1
|
step: 0.1
|
||||||
mode: box
|
mode: box
|
||||||
- name: graph_3_fill_opacity
|
- name: graph_3_fill_opacity
|
||||||
label: ③ Graph · Fläche
|
label: Fläche
|
||||||
selector:
|
selector:
|
||||||
number:
|
number:
|
||||||
min: 0
|
min: 0
|
||||||
max: 1
|
max: 1
|
||||||
step: 0.05
|
step: 0.05
|
||||||
mode: slider
|
mode: slider
|
||||||
|
- name: graph_3_value_mode
|
||||||
- name: line_width
|
label: Wert-Modus
|
||||||
label: Anzeige · Standard Linien Dicke
|
|
||||||
selector:
|
selector:
|
||||||
number:
|
select:
|
||||||
min: 0.5
|
options:
|
||||||
max: 10
|
- label: Originalwert
|
||||||
step: 0.1
|
value: default
|
||||||
mode: box
|
- label: Prozent von Maximalwert
|
||||||
- name: line_opacity
|
value: percent_of_max
|
||||||
label: Anzeige · Linien Sichtbarkeit
|
- name: graph_3_max_value
|
||||||
|
label: Maximalwert für Prozent
|
||||||
selector:
|
selector:
|
||||||
number:
|
number:
|
||||||
min: 0
|
min: 0
|
||||||
max: 1
|
max: 100000
|
||||||
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
|
|
||||||
step: 1
|
step: 1
|
||||||
mode: box
|
mode: box
|
||||||
|
|
||||||
|
- type: expandable
|
||||||
|
name: advanced_settings
|
||||||
|
title: Erweitert
|
||||||
|
icon: mdi:tune-variant
|
||||||
|
expanded: false
|
||||||
|
schema:
|
||||||
- name: extend_to_now
|
- name: extend_to_now
|
||||||
label: Erweitert · Linie bis jetzt verlängern
|
label: Linie bis jetzt verlängern
|
||||||
selector:
|
selector:
|
||||||
boolean: null
|
boolean: null
|
||||||
- name: extend_threshold_minutes
|
- name: extend_threshold_minutes
|
||||||
label: Erweitert · Verlängern nach Minuten
|
label: Verlängern nach Minuten
|
||||||
selector:
|
selector:
|
||||||
number:
|
number:
|
||||||
min: 0
|
min: 0
|
||||||
@@ -857,7 +1186,7 @@ unique_bubble_graph:
|
|||||||
step: 1
|
step: 1
|
||||||
mode: box
|
mode: box
|
||||||
- name: max_points
|
- name: max_points
|
||||||
label: Erweitert · Max. Datenpunkte
|
label: Max. Datenpunkte
|
||||||
selector:
|
selector:
|
||||||
number:
|
number:
|
||||||
min: 20
|
min: 20
|
||||||
@@ -865,11 +1194,11 @@ unique_bubble_graph:
|
|||||||
step: 10
|
step: 10
|
||||||
mode: box
|
mode: box
|
||||||
- name: tooltip
|
- name: tooltip
|
||||||
label: Erweitert · Tooltip anzeigen
|
label: Tooltip anzeigen
|
||||||
selector:
|
selector:
|
||||||
boolean: null
|
boolean: null
|
||||||
- name: tooltip_top
|
- name: tooltip_top
|
||||||
label: Erweitert · Tooltip Position oben
|
label: Tooltip Position oben
|
||||||
selector:
|
selector:
|
||||||
number:
|
number:
|
||||||
min: 0
|
min: 0
|
||||||
|
|||||||
Reference in New Issue
Block a user