Files
ui/src/features/history-chart/historyChartSeries.ts

140 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type {
CustomSeriesRenderItemAPI,
CustomSeriesRenderItemParams,
CustomSeriesRenderItemReturn,
SeriesOption,
} from 'echarts';
import { graphic } from 'echarts/core';
import type { HistoryPoint } from '../../entities/history/types';
import { parseGranulateMs } from '../../utils/historyGranularity';
import type { HistoryBucketValue } from './chartTypes';
export const SERIES_COLORS = [
'#5B8FF9',
'#5AD8A6',
'#F6BD16',
'#E8684A',
'#6DC8EC',
'#FF9D4D',
'#73D13D',
'#A7B3FF',
];
type CartesianCoordSys = {
x: number;
y: number;
width: number;
height: number;
};
function isFinitePoint(point: HistoryPoint): boolean {
return [point.avg_value, point.min_value, point.max_value].every(Number.isFinite);
}
/** Упаковывает один bucket API в компактный tuple для custom series ECharts. */
function createBucketData(points: HistoryPoint[], slotMs: number): HistoryBucketValue[] {
return points.filter(isFinitePoint).map((point) => [
new Date(point.time).getTime(),
point.avg_value,
point.min_value,
point.max_value,
point.point_count,
slotMs,
]);
}
/** Рисует один bucket как временной слот: затухающий min..max и яркий маркер avg. */
function renderBucket(color: string) {
return (params: CustomSeriesRenderItemParams, api: CustomSeriesRenderItemAPI): CustomSeriesRenderItemReturn => {
const time = Number(api.value(0));
const avg = Number(api.value(1));
const min = Number(api.value(2));
const max = Number(api.value(3));
const slotMs = Number(api.value(5));
// ECharts отдает размеры области графика в runtime, но в публичных типах
// они не описаны. Поэтому держим cast локально рядом с clipping-логикой.
const coordSys = params.coordSys as unknown as CartesianCoordSys;
// Переводим координаты данных (time/value) в пиксели canvas.
const avgPoint = api.coord([time, avg]);
const minPoint = api.coord([time, min]);
const maxPoint = api.coord([time, max]);
const slotStart = api.coord([time - slotMs / 2, avg]);
const slotEnd = api.coord([time + slotMs / 2, avg]);
// Слот занимает один интервал грануляции по X и диапазон min..max по Y.
const x = Math.min(slotStart[0], slotEnd[0]);
const rawHeight = Math.abs(maxPoint[1] - minPoint[1]);
const width = Math.max(2, Math.abs(slotEnd[0] - slotStart[0]));
const height = Math.max(2, rawHeight);
const y = Math.min(minPoint[1], maxPoint[1]) - (height - rawHeight) / 2;
const avgY = avgPoint[1];
const chartArea = { x: coordSys.x, y: coordSys.y, width: coordSys.width, height: coordSys.height };
// Обрезаем фигуры, чтобы при Y-зуме они не выходили за область графика.
const rangeShape = graphic.clipRectByRect(
{ x, y, width, height },
chartArea,
);
const avgShape = graphic.clipRectByRect(
{ x, y: avgY - 1, width, height: 2 },
chartArea,
);
if (!rangeShape) {
return null;
}
return {
type: 'group',
children: [
{
type: 'rect',
shape: rangeShape,
style: {
fill: new graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: `${color}00` },
{ offset: 0.5, color: `${color}66` },
{ offset: 1, color: `${color}00` },
]),
},
},
...(avgShape ? [{
type: 'rect' as const,
shape: avgShape,
style: {
fill: color,
},
}] : []),
],
};
};
}
/** Создает отдельный временной слот min..max с акцентом на avg для каждой точки history. */
export function createSeriesOptions(points: HistoryPoint[], index: number, label: string, granulate: string): SeriesOption[] {
const color = SERIES_COLORS[index % SERIES_COLORS.length];
const data = createBucketData(points, parseGranulateMs(granulate));
return [
{
name: label,
type: 'custom',
data,
renderItem: renderBucket(color),
encode: {
x: 0,
y: [1, 2, 3],
},
itemStyle: {
color,
},
emphasis: {
disabled: true,
},
z: 3,
} as SeriesOption,
];
}