From 3024ff90b0eb0d3c53dfa669982853c3f98441e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B2=D0=BE=D0=B2=20=D0=90=D1=80=D1=82?= =?UTF-8?q?=D0=B5=D0=BC?= Date: Sun, 5 Jul 2026 23:53:16 +0300 Subject: [PATCH] =?UTF-8?q?BUR-62=20=D0=98=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=20=D1=82=D1=83=D0=BB=D1=82=D0=B8=D0=BF=20=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=B3=D1=80=D0=B0=D1=84=D0=B8=D0=BA=D0=B5.=20=D0=A1=D0=BA?= =?UTF-8?q?=D0=BE=D1=80=D1=80=D0=B5=D0=BA=D1=82=D0=B8=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=20=D0=B3=D1=80=D0=B0=D0=B4=D0=B8=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=20=D0=BD=D0=B0=20=D0=B3=D1=80=D0=B0=D1=84=D0=B8=D0=BA=D0=B5=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20min-avg-max.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/history-chart/chartTypes.ts | 4 +- .../history-chart/historyChartFormat.ts | 61 ++++++++++++++++--- .../history-chart/historyChartSeries.ts | 24 ++++++-- 3 files changed, 74 insertions(+), 15 deletions(-) diff --git a/src/features/history-chart/chartTypes.ts b/src/features/history-chart/chartTypes.ts index a9ae1f2..f190479 100644 --- a/src/features/history-chart/chartTypes.ts +++ b/src/features/history-chart/chartTypes.ts @@ -1,8 +1,8 @@ import type { HistoryAxisLabelFormat } from '../../utils/historyGranularity'; -export type AvgPointValue = [time: number, avg: number, min: number, max: number, count: number]; +export type AvgPointValue = [time: number, avg: number, min: number, max: number, count: number, slotMs?: number]; -export type HistoryBucketValue = [...AvgPointValue, slotMs: number]; +export type HistoryBucketValue = [time: number, avg: number, min: number, max: number, count: number, slotMs: number]; export type HistoryZoomRange = { from: string; diff --git a/src/features/history-chart/historyChartFormat.ts b/src/features/history-chart/historyChartFormat.ts index 5e2b047..c7dd9ce 100644 --- a/src/features/history-chart/historyChartFormat.ts +++ b/src/features/history-chart/historyChartFormat.ts @@ -60,6 +60,58 @@ function formatChartValue(value: number): string { }).format(value); } +/** Форматирует полную дату и время для начала/конца интервала в tooltip. */ +function formatTooltipDateTime(value: number): string { + return new Intl.DateTimeFormat('ru-RU', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }).format(new Date(value)); +} + +/** Форматирует только время, когда конец интервала находится в тот же день. */ +function formatTooltipTime(value: number): string { + return new Intl.DateTimeFormat('ru-RU', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }).format(new Date(value)); +} + +/** Проверяет, можно ли показать конец интервала без повторения даты. */ +function isSameDate(first: number, second: number): boolean { + const firstDate = new Date(first); + const secondDate = new Date(second); + + return ( + firstDate.getFullYear() === secondDate.getFullYear() + && firstDate.getMonth() === secondDate.getMonth() + && firstDate.getDate() === secondDate.getDate() + ); +} + +/** Показывает bucket графика как интервал текущей грануляции, а не как одну точку времени. */ +function formatTooltipInterval(value: AvgPointValue): string { + const time = value[0]; + const slotMs = value[5]; + + if (!slotMs) { + return formatTooltipDateTime(time); + } + + const from = time - slotMs / 2; + const to = time + slotMs / 2; + + if (isSameDate(from, to)) { + return `${formatTooltipDateTime(from)} — ${formatTooltipTime(to)}`; + } + + return `${formatTooltipDateTime(from)} — ${formatTooltipDateTime(to)}`; +} + /** Форматирует значение tooltip: одно число для min=avg=max или среднее с диапазоном. */ function formatTooltipValue(value: AvgPointValue): string { const avg = formatChartValue(value[1]); @@ -89,14 +141,7 @@ export function formatTooltip(params: unknown): string { return ''; } - const header = new Intl.DateTimeFormat('ru-RU', { - day: '2-digit', - month: '2-digit', - year: 'numeric', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - }).format(new Date(firstValue[0])); + const header = formatTooltipInterval(firstValue); const rows = avgItems.map((item) => { const value = item.value as AvgPointValue; diff --git a/src/features/history-chart/historyChartSeries.ts b/src/features/history-chart/historyChartSeries.ts index d35fb15..d4daccc 100644 --- a/src/features/history-chart/historyChartSeries.ts +++ b/src/features/history-chart/historyChartSeries.ts @@ -27,6 +27,22 @@ type CartesianCoordSys = { height: number; }; +function clamp(value: number, min: number, max: number): number { + return Math.min(max, Math.max(min, value)); +} + +function createAvgGradientStops(color: string, avgOffset: number) { + const softZone = 0.18; + + return [ + { offset: 0, color: `${color}00` }, + { offset: clamp(avgOffset - softZone, 0, 1), color: `${color}22` }, + { offset: avgOffset, color: `${color}70` }, + { offset: clamp(avgOffset + softZone, 0, 1), color: `${color}22` }, + { offset: 1, color: `${color}00` }, + ].sort((first, second) => first.offset - second.offset); +} + function isFinitePoint(point: HistoryPoint): boolean { return [point.avg_value, point.min_value, point.max_value].every(Number.isFinite); } @@ -86,6 +102,8 @@ function renderBucket(color: string) { return null; } + const avgOffset = clamp((avgY - rangeShape.y) / rangeShape.height, 0, 1); + return { type: 'group', children: [ @@ -93,11 +111,7 @@ function renderBucket(color: string) { 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` }, - ]), + fill: new graphic.LinearGradient(0, 0, 0, 1, createAvgGradientStops(color, avgOffset)), }, }, ...(avgShape ? [{ -- 2.49.1