Custom variable down sampling for line charts (#25561)

This commit is contained in:
Petar Petrov 2025-05-23 16:20:41 +03:00 committed by GitHub
parent 4d8176ad6e
commit 81ba2db93a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 105 additions and 14 deletions

View File

@ -0,0 +1,72 @@
import type { LineSeriesOption } from "echarts";
export function downSampleLineData(
data: LineSeriesOption["data"],
chartWidth: number,
minX?: number,
maxX?: number
) {
if (!data || data.length < 10) {
return data;
}
const width = chartWidth * window.devicePixelRatio;
if (data.length <= width) {
return data;
}
const min = minX ?? getPointData(data[0]!)[0];
const max = maxX ?? getPointData(data[data.length - 1]!)[0];
const step = Math.floor((max - min) / width);
const frames = new Map<
number,
{
min: { point: (typeof data)[number]; x: number; y: number };
max: { point: (typeof data)[number]; x: number; y: number };
}
>();
// Group points into frames
for (const point of data) {
const pointData = getPointData(point);
if (!Array.isArray(pointData)) continue;
const x = Number(pointData[0]);
const y = Number(pointData[1]);
if (isNaN(x) || isNaN(y)) continue;
const frameIndex = Math.floor((x - min) / step);
const frame = frames.get(frameIndex);
if (!frame) {
frames.set(frameIndex, { min: { point, x, y }, max: { point, x, y } });
} else {
if (frame.min.y > y) {
frame.min = { point, x, y };
}
if (frame.max.y < y) {
frame.max = { point, x, y };
}
}
}
// Convert frames back to points
const result: typeof data = [];
for (const [_i, frame] of frames) {
// Use min/max points to preserve visual accuracy
// The order of the data must be preserved so max may be before min
if (frame.min.x > frame.max.x) {
result.push(frame.max.point);
}
result.push(frame.min.point);
if (frame.min.x < frame.max.x) {
result.push(frame.max.point);
}
}
return result;
}
function getPointData(point: NonNullable<LineSeriesOption["data"]>[number]) {
const pointData =
point && typeof point === "object" && "value" in point
? point.value
: point;
return pointData as number[];
}

View File

@ -27,6 +27,7 @@ import "../ha-icon-button";
import { formatTimeLabel } from "./axis-label"; import { formatTimeLabel } from "./axis-label";
import { ensureArray } from "../../common/array/ensure-array"; import { ensureArray } from "../../common/array/ensure-array";
import "../chips/ha-assist-chip"; import "../chips/ha-assist-chip";
import { downSampleLineData } from "./down-sample";
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000; export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
const LEGEND_OVERFLOW_LIMIT = 10; const LEGEND_OVERFLOW_LIMIT = 10;
@ -613,19 +614,21 @@ export class HaChartBase extends LitElement {
} }
private _getSeries() { private _getSeries() {
const series = ensureArray(this.data).filter( const xAxis = (this.options?.xAxis?.[0] ?? this.options?.xAxis) as
(d) => !this._hiddenDatasets.has(String(d.name ?? d.id)) | XAXisOption
); | undefined;
const yAxis = (this.options?.yAxis?.[0] ?? this.options?.yAxis) as const yAxis = (this.options?.yAxis?.[0] ?? this.options?.yAxis) as
| YAXisOption | YAXisOption
| undefined; | undefined;
const series = ensureArray(this.data)
.filter((d) => !this._hiddenDatasets.has(String(d.name ?? d.id)))
.map((s) => {
if (s.type === "line") {
if (yAxis?.type === "log") { if (yAxis?.type === "log") {
// set <=0 values to null so they render as gaps on a log graph // set <=0 values to null so they render as gaps on a log graph
return series.map((d) => return {
d.type === "line" ...s,
? { data: s.data?.map((v) =>
...d,
data: d.data?.map((v) =>
Array.isArray(v) Array.isArray(v)
? [ ? [
v[0], v[0],
@ -634,10 +637,26 @@ export class HaChartBase extends LitElement {
] ]
: v : v
), ),
};
} }
: d if (s.sampling === "minmax") {
); const minX =
xAxis?.min && typeof xAxis.min === "number"
? xAxis.min
: undefined;
const maxX =
xAxis?.max && typeof xAxis.max === "number"
? xAxis.max
: undefined;
return {
...s,
sampling: undefined,
data: downSampleLineData(s.data, this.clientWidth, minX, maxX),
};
} }
}
return s;
});
return series; return series;
} }