mirror of
https://github.com/home-assistant/frontend.git
synced 2026-04-02 00:27:49 +01:00
* Rescale Y-axis on chart zoom via custom AxisProxy filterMode Patch ECharts' AxisProxy.filterData to support a "boundaryFilter" mode that keeps the nearest data point outside each zoom boundary while filtering distant points. This lets ECharts natively rescale the Y-axis to the visible data range without causing line gaps at the zoom edges. * Add tests for ECharts AxisProxy patch internals Verify that the ECharts internals our boundaryFilter patch relies on still exist (filterData, getTargetSeriesModels on AxisProxy prototype), and test the patch behavior: delegation for other filterModes, early return for non-matching models, and correct boundary-preserving filtering. * Update comment
142 lines
4.8 KiB
TypeScript
142 lines
4.8 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import AxisProxy from "echarts/lib/component/dataZoom/AxisProxy";
|
|
|
|
/**
|
|
* These tests verify that the ECharts internals our axis-proxy-patch relies on
|
|
* still exist. If an ECharts upgrade changes these, the tests will fail,
|
|
* alerting us that the patch in src/resources/echarts/axis-proxy-patch.ts
|
|
* needs to be updated.
|
|
*/
|
|
describe("ECharts internals required by axis-proxy-patch", () => {
|
|
it("AxisProxy has a filterData method on its prototype", () => {
|
|
expect(typeof (AxisProxy as any).prototype.filterData).toBe("function");
|
|
});
|
|
|
|
it("AxisProxy prototype exposes the expected instance fields pattern", () => {
|
|
// The patch accesses these properties via `this` inside filterData:
|
|
// this._dataZoomModel, this._dimName, this._valueWindow,
|
|
// this.getTargetSeriesModels()
|
|
// We can't easily construct a real AxisProxy, but we can verify
|
|
// getTargetSeriesModels exists on the prototype.
|
|
expect(typeof (AxisProxy as any).prototype.getTargetSeriesModels).toBe(
|
|
"function"
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("axis-proxy-patch applies boundaryFilter mode", () => {
|
|
it("patches filterData to handle boundaryFilter", async () => {
|
|
// Import the patch (side-effect module)
|
|
await import("../../../src/resources/echarts/axis-proxy-patch");
|
|
|
|
const filterData = (AxisProxy as any).prototype.filterData;
|
|
|
|
// Create a mock dataZoomModel that requests boundaryFilter
|
|
const mockDataZoomModel = {
|
|
get: (key: string) =>
|
|
key === "filterMode" ? "boundaryFilter" : undefined,
|
|
};
|
|
|
|
// The patched filterData should not throw when called with
|
|
// boundaryFilter and a non-matching _dataZoomModel (early return path)
|
|
const mockProxy = {
|
|
_dataZoomModel: "different-model",
|
|
};
|
|
|
|
// Should return early because dataZoomModel !== this._dataZoomModel
|
|
expect(() =>
|
|
filterData.call(mockProxy, mockDataZoomModel, {})
|
|
).not.toThrow();
|
|
});
|
|
|
|
it("falls through to original filterData for other filterModes", async () => {
|
|
await import("../../../src/resources/echarts/axis-proxy-patch");
|
|
|
|
const filterData = (AxisProxy as any).prototype.filterData;
|
|
|
|
// Temporarily replace the original to verify delegation
|
|
const calls: any[] = [];
|
|
|
|
// The patched function stores the original in its closure.
|
|
// We can verify it doesn't enter the boundaryFilter path by checking
|
|
// that no boundary-specific logic runs (no getTargetSeriesModels call).
|
|
const mockDataZoomModel = {
|
|
get: (key: string) => (key === "filterMode" ? "filter" : undefined),
|
|
};
|
|
|
|
const mockProxy = {
|
|
getTargetSeriesModels: () => {
|
|
calls.push("getTargetSeriesModels");
|
|
return [];
|
|
},
|
|
};
|
|
|
|
// Should not throw — the original filterData handles non-matching gracefully
|
|
expect(() =>
|
|
filterData.call(mockProxy, mockDataZoomModel, {})
|
|
).not.toThrow();
|
|
|
|
// getTargetSeriesModels should NOT have been called because the
|
|
// patched function delegates to the original for filterMode !== "boundaryFilter"
|
|
expect(calls).toEqual([]);
|
|
});
|
|
|
|
it("filters data keeping boundary points", async () => {
|
|
await import("../../../src/resources/echarts/axis-proxy-patch");
|
|
|
|
const filterData = (AxisProxy as any).prototype.filterData;
|
|
|
|
// Simulate data: timestamps [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
|
// Zoom window: [3, 7]
|
|
// Expected: keep index 1 (value 2, left boundary), indices 2-6 (in window),
|
|
// index 7 (value 8, right boundary), filter out 0, 8, 9
|
|
const values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
|
const kept: number[] = [];
|
|
|
|
const mockStore = {
|
|
get: (_dimIndex: number, i: number) => values[i],
|
|
};
|
|
|
|
const mockSeriesData = {
|
|
mapDimensionsAll: () => ["x"],
|
|
getStore: () => mockStore,
|
|
getDimensionIndex: () => 0,
|
|
count: () => values.length,
|
|
filterSelf: (fn: (idx: number) => boolean) => {
|
|
for (let i = 0; i < values.length; i++) {
|
|
if (fn(i)) {
|
|
kept.push(i);
|
|
}
|
|
}
|
|
},
|
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
setApproximateExtent: () => {},
|
|
};
|
|
|
|
const mockSeriesModel = {
|
|
getData: () => mockSeriesData,
|
|
};
|
|
|
|
const mockDataZoomModel = {
|
|
get: (key: string) =>
|
|
key === "filterMode" ? "boundaryFilter" : undefined,
|
|
};
|
|
|
|
const mockProxy = {
|
|
_dataZoomModel: mockDataZoomModel,
|
|
_dimName: "x",
|
|
_valueWindow: [3, 7],
|
|
getTargetSeriesModels: () => [mockSeriesModel],
|
|
};
|
|
|
|
filterData.call(mockProxy, mockDataZoomModel, {});
|
|
|
|
// Index 0 (value 1): filtered out
|
|
// Index 1 (value 2): left boundary (nearest < 3)
|
|
// Index 2-6 (values 3-7): in window
|
|
// Index 7 (value 8): right boundary (nearest > 7)
|
|
// Index 8-9 (values 9-10): filtered out
|
|
expect(kept).toEqual([1, 2, 3, 4, 5, 6, 7]);
|
|
});
|
|
});
|