Добавил возможность выбора множества тегов для отображения.

This commit is contained in:
Первов Артем
2026-06-22 21:49:52 +03:00
parent 6ab9cd1c75
commit 9fc66624dc
10 changed files with 229 additions and 109 deletions

View File

@@ -1,92 +1,70 @@
import { LineChart, ScatterChart } from 'echarts/charts';
import {
DataZoomComponent,
GridComponent,
LegendComponent,
TooltipComponent,
} from 'echarts/components';
import * as echarts from 'echarts/core';
import { CanvasRenderer } from 'echarts/renderers';
import ReactEChartsCore from 'echarts-for-react/esm/core.js';
import { useMemo, useRef } from 'react';
import type { HistoryResponse } from '../../entities/history/types';
import { useCallback, useMemo, useRef } from 'react';
import type { HistoryAxisLabelFormat } from '../../utils/historyGranularity';
import type { DataZoomEventBatch, DataZoomState } from './chartTypes';
import { HistoryChartArea } from './HistoryChartArea';
import { createHistoryChartOptions } from './historyChartOptions';
echarts.use([
LineChart,
ScatterChart,
GridComponent,
LegendComponent,
TooltipComponent,
DataZoomComponent,
CanvasRenderer,
]);
import { useHistoryChartQueries } from './useHistoryChartQueries';
import { useHistoryChartSeries } from './useHistoryChartSeries';
type HistoryChartProps = {
data?: HistoryResponse;
loading: boolean;
from?: string;
to?: string;
tickIntervalMs?: number;
edge: string;
from: string;
granulate: string;
labelFormat?: HistoryAxisLabelFormat;
tag?: string;
tags: string[];
tickIntervalMs?: number;
to: string;
tagLabels?: Record<string, string>;
};
/** Рисует avg-линию, min/max-точки и вертикальный диапазон для агрегированной истории. */
/** Контейнер истории: загружает выбранные теги и передает готовые серии в область ECharts. */
export function HistoryChart({
data,
loading,
edge,
from,
to,
tickIntervalMs,
granulate,
labelFormat,
tag,
tags,
tickIntervalMs,
to,
tagLabels = {},
}: HistoryChartProps) {
const dataZoomRef = useRef<DataZoomState | null>(null);
const seriesLabel = tag ? tagLabels[tag] ?? tag : '';
const legendData = useMemo(() => (data?.rows.length && seriesLabel ? [seriesLabel] : []), [data?.rows.length, seriesLabel]);
const lines = useHistoryChartQueries({ edge, from, granulate, tags, to, tagLabels });
const series = useHistoryChartSeries(lines);
const legendData = useMemo(() => lines.map((line) => line.label), [lines]);
const loading = lines.some((line) => line.loading);
const option = useMemo(
() =>
createHistoryChartOptions({
data,
dataZoomState: dataZoomRef.current ?? {},
from,
to,
labelFormat,
legendData,
seriesLabel,
series,
tickIntervalMs,
}),
[data, from, labelFormat, legendData, seriesLabel, tickIntervalMs, to],
[from, labelFormat, legendData, series, tickIntervalMs, to],
);
const onChartEvents = useMemo(
() => ({
datazoom: (event: DataZoomEventBatch) => {
const state = event.batch?.[0] ?? event;
dataZoomRef.current = {
start: state.start,
end: state.end,
startValue: state.startValue,
endValue: state.endValue,
};
},
}),
[],
const handleDataZoom = useCallback((event: DataZoomEventBatch) => {
const state = event.batch?.[0] ?? event;
dataZoomRef.current = {
start: state.start,
end: state.end,
startValue: state.startValue,
endValue: state.endValue,
};
}, []);
return (
<HistoryChartArea
hasData={series.length > 0}
hasSelection={tags.length > 0}
loading={loading}
option={option}
onDataZoom={handleDataZoom}
/>
);
if (loading) {
return <div className="chart-placeholder">Загрузка графика...</div>;
}
if (!data?.rows.length) {
return <div className="chart-placeholder">Нет данных для выбранного диапазона</div>;
}
return <ReactEChartsCore echarts={echarts} option={option} className="history-chart" onEvents={onChartEvents} lazyUpdate />;
}

View File

@@ -0,0 +1,43 @@
import type { EChartsOption } from 'echarts';
import ReactEChartsCore from 'echarts-for-react/esm/core.js';
import { useMemo } from 'react';
import type { DataZoomEventBatch } from './chartTypes';
import { echarts } from './historyChartEcharts';
type HistoryChartAreaProps = {
hasData: boolean;
hasSelection: boolean;
loading: boolean;
option: EChartsOption;
onDataZoom: (event: DataZoomEventBatch) => void;
};
/** Отвечает только за визуальную область графика: placeholder или ECharts canvas. */
export function HistoryChartArea({
hasData,
hasSelection,
loading,
option,
onDataZoom,
}: HistoryChartAreaProps) {
const onChartEvents = useMemo(
() => ({
datazoom: onDataZoom,
}),
[onDataZoom],
);
if (!hasSelection) {
return <div className="chart-placeholder">Разверните список показателей и выберите серию для графика</div>;
}
if (loading) {
return <div className="chart-placeholder">Загрузка графика...</div>;
}
if (!hasData) {
return <div className="chart-placeholder">Нет данных для выбранного диапазона</div>;
}
return <ReactEChartsCore echarts={echarts} option={option} className="history-chart" onEvents={onChartEvents} lazyUpdate />;
}

View File

@@ -0,0 +1,21 @@
import { LineChart, ScatterChart } from 'echarts/charts';
import {
DataZoomComponent,
GridComponent,
LegendComponent,
TooltipComponent,
} from 'echarts/components';
import * as echarts from 'echarts/core';
import { CanvasRenderer } from 'echarts/renderers';
echarts.use([
LineChart,
ScatterChart,
GridComponent,
LegendComponent,
TooltipComponent,
DataZoomComponent,
CanvasRenderer,
]);
export { echarts };

View File

@@ -1,29 +1,27 @@
import type { EChartsOption } from 'echarts';
import type { HistoryResponse } from '../../entities/history/types';
import type { SeriesOption } from 'echarts';
import type { HistoryAxisLabelFormat } from '../../utils/historyGranularity';
import type { DataZoomState } from './chartTypes';
import { formatAxisDate, formatTooltip } from './historyChartFormat';
import { createSeriesOptions, SERIES_COLORS } from './historyChartSeries';
import { SERIES_COLORS } from './historyChartSeries';
export type HistoryChartOptionsInput = {
data?: HistoryResponse;
dataZoomState: DataZoomState;
from?: string;
to?: string;
labelFormat?: HistoryAxisLabelFormat;
legendData: string[];
seriesLabel: string;
series: SeriesOption[];
tickIntervalMs?: number;
};
export function createHistoryChartOptions({
data,
dataZoomState,
from,
to,
labelFormat,
legendData,
seriesLabel,
series,
tickIntervalMs,
}: HistoryChartOptionsInput): EChartsOption {
const xMin = from ? new Date(from).getTime() : undefined;
@@ -102,6 +100,6 @@ export function createHistoryChartOptions({
endValue: dataZoomState.endValue,
},
],
series: data?.rows.length ? createSeriesOptions(data.rows, 0, seriesLabel) : [],
series,
};
}

View File

@@ -31,7 +31,7 @@ export function createSeriesOptions(points: HistoryPoint[], index: number, label
return [
{
name: `${label} min/max`,
name: label,
type: 'line',
data: spreadData,
showSymbol: false,
@@ -50,7 +50,7 @@ export function createSeriesOptions(points: HistoryPoint[], index: number, label
z: 1,
},
{
name: `${label} min`,
name: label,
type: 'scatter',
data: points.map((point) => [new Date(point.time).getTime(), point.min_value]),
symbolSize: 4,
@@ -66,7 +66,7 @@ export function createSeriesOptions(points: HistoryPoint[], index: number, label
z: 2,
},
{
name: `${label} max`,
name: label,
type: 'scatter',
data: points.map((point) => [new Date(point.time).getTime(), point.max_value]),
symbolSize: 4,

View File

@@ -0,0 +1,52 @@
import { useMemo } from 'react';
import { useQueries } from '@tanstack/react-query';
import { getHistory } from '../../entities/history/api';
import type { HistoryPoint } from '../../entities/history/types';
type UseHistoryChartQueriesParams = {
edge: string;
from: string;
granulate: string;
tags: string[];
to: string;
tagLabels: Record<string, string>;
};
export type HistoryChartLineData = {
index: number;
label: string;
loading: boolean;
rows: HistoryPoint[];
tag: string;
};
export function useHistoryChartQueries({
edge,
from,
granulate,
tags,
to,
tagLabels,
}: UseHistoryChartQueriesParams): HistoryChartLineData[] {
const enabled = Boolean(edge && from && granulate && to);
const queries = useQueries({
queries: tags.map((tag) => ({
queryKey: ['history', edge, tag, from, to, granulate],
queryFn: () => getHistory({ edge, tag, from, to, granulate }),
enabled,
})),
});
return useMemo(
() =>
tags.map((tag, index) => ({
index,
label: tagLabels[tag] ?? tag,
loading: enabled && queries[index].isPending,
rows: queries[index].data?.rows ?? [],
tag,
})),
[enabled, queries, tagLabels, tags],
);
}

View File

@@ -0,0 +1,14 @@
import { useMemo } from 'react';
import type { SeriesOption } from 'echarts';
import { createSeriesOptions } from './historyChartSeries';
import type { HistoryChartLineData } from './useHistoryChartQueries';
export function useHistoryChartSeries(lines: HistoryChartLineData[]): SeriesOption[] {
return useMemo(
() =>
lines.flatMap((line) =>
line.rows.length ? createSeriesOptions(line.rows, line.index, line.label) : [],
),
[lines],
);
}