Сглаживающая линия #28
@@ -4,6 +4,8 @@ import { useRef } from 'react';
|
||||
import { CalendarDays, CalendarClock, Check, ChevronDown, Clock3, DatabaseZap, Search } from 'lucide-react';
|
||||
import type { CurrentItem } from '../../../entities/current/types';
|
||||
import { HistoryChart } from '../../../features/history-chart/HistoryChart';
|
||||
import { HistoryChartAvgLineModeControl } from '../../../features/history-chart/HistoryChartAvgLineModeControl';
|
||||
import type { AvgLineMode } from '../../../features/history-chart/chartTypes';
|
||||
import { RANGE_PRESETS, createRange, type DateRangeState } from '../../../features/history/dateRange';
|
||||
import { formatNumber, toIsoFromInput } from '../../../utils/format';
|
||||
import type { HistoryGranularity } from '../../../utils/historyGranularity';
|
||||
@@ -93,6 +95,7 @@ export function ArchiveView({
|
||||
onSelectVisibleTags,
|
||||
onToggleTag,
|
||||
}: ArchiveViewProps) {
|
||||
const [avgLineMode, setAvgLineMode] = useState<AvgLineMode>('auto');
|
||||
const [selectorOpen, setSelectorOpen] = useState(false);
|
||||
const selectedPreview = selectedTags.slice(0, 10);
|
||||
const hiddenSelectedCount = Math.max(0, selectedTags.length - selectedPreview.length);
|
||||
@@ -209,7 +212,7 @@ export function ArchiveView({
|
||||
onPointerEnter={() => setSelectorOpen(false)}
|
||||
>
|
||||
<div className="toolbar">
|
||||
<div className="segmented">
|
||||
<div className="segmented segmented--range-preset">
|
||||
{RANGE_PRESETS.map((preset) => (
|
||||
<button key={preset.id} type="button" onClick={() => onRangeChange(createRange(preset.hours))}>
|
||||
{preset.label}
|
||||
@@ -250,11 +253,13 @@ export function ArchiveView({
|
||||
onChange={(value) => updateRangePart('to', 'time', value)}
|
||||
/>
|
||||
</div>
|
||||
<HistoryChartAvgLineModeControl value={avgLineMode} onChange={setAvgLineMode} />
|
||||
</div>
|
||||
|
||||
{selectedTags.length ? (
|
||||
<HistoryChart
|
||||
key={`${range.from}:${range.to}:${historyAxis.granulate}`}
|
||||
key={`${range.from}:${range.to}:${historyAxis.granulate}:${historyAxis.tickIntervalMs ?? ''}`}
|
||||
avgLineMode={avgLineMode}
|
||||
edge={edgeId}
|
||||
from={fromIso}
|
||||
to={toIso}
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useHistoryChartQueries } from './useHistoryChartQueries';
|
||||
import { useHistoryChartSeries } from './useHistoryChartSeries';
|
||||
|
||||
type HistoryChartProps = {
|
||||
avgLineMode: AvgLineMode;
|
||||
edge: string;
|
||||
from: string;
|
||||
granulate: string;
|
||||
@@ -42,6 +43,7 @@ function shouldShowAvgLine(mode: AvgLineMode, range: HistoryZoomRange): boolean
|
||||
}
|
||||
|
||||
export function HistoryChart({
|
||||
avgLineMode,
|
||||
edge,
|
||||
from,
|
||||
granulate,
|
||||
@@ -62,7 +64,6 @@ export function HistoryChart({
|
||||
const zoomRangeRef = useRef(zoomRange);
|
||||
const zoomTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const lastReadyOptionRef = useRef<EChartsOption | null>(null);
|
||||
const [avgLineMode, setAvgLineMode] = useState<AvgLineMode>('auto');
|
||||
const lines = useHistoryChartQueries({
|
||||
edge,
|
||||
from: zoomRange.from,
|
||||
@@ -149,13 +150,12 @@ export function HistoryChart({
|
||||
|
||||
return (
|
||||
<HistoryChartArea
|
||||
avgLineMode={avgLineMode}
|
||||
hasData={displayHasData}
|
||||
hasSelection={tags.length > 0}
|
||||
loading={loading}
|
||||
avgLineMode={avgLineMode}
|
||||
option={displayOption}
|
||||
onDataZoom={handleDataZoom}
|
||||
onAvgLineModeChange={setAvgLineMode}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@ import type { EChartsOption } from 'echarts';
|
||||
import ReactEChartsCore from 'echarts-for-react/esm/core.js';
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import type { WheelEvent } from 'react';
|
||||
import type { AvgLineMode, DataZoomEventBatch } from './chartTypes';
|
||||
import type { DataZoomEventBatch } from './chartTypes';
|
||||
import { echarts } from './historyChartEcharts';
|
||||
import type { AvgLineMode } from './chartTypes';
|
||||
|
||||
type HistoryChartAreaProps = {
|
||||
avgLineMode: AvgLineMode;
|
||||
@@ -11,17 +12,9 @@ type HistoryChartAreaProps = {
|
||||
hasSelection: boolean;
|
||||
loading: boolean;
|
||||
option: EChartsOption;
|
||||
onAvgLineModeChange: (mode: AvgLineMode) => void;
|
||||
onDataZoom: (event: DataZoomEventBatch) => void;
|
||||
};
|
||||
|
||||
// Порядок режимов соответствует требованию: авто, принудительно рисовать, принудительно скрыть.
|
||||
const AVG_LINE_MODES: Array<{ value: AvgLineMode; label: string }> = [
|
||||
{ value: 'auto', label: 'Авто' },
|
||||
{ value: 'show', label: 'Рисовать' },
|
||||
{ value: 'hide', label: 'Не рисовать' },
|
||||
];
|
||||
|
||||
/** Включает или выключает нативный X-зум колесом, чтобы Shift + колесо работал только по Y. */
|
||||
function setXAxisWheelZoom(chart: ReturnType<ReactEChartsCore['getEchartsInstance']>, enabled: boolean): void {
|
||||
chart.setOption(
|
||||
@@ -44,7 +37,6 @@ export function HistoryChartArea({
|
||||
hasSelection,
|
||||
loading,
|
||||
option,
|
||||
onAvgLineModeChange,
|
||||
onDataZoom,
|
||||
}: HistoryChartAreaProps) {
|
||||
const chartRef = useRef<InstanceType<typeof ReactEChartsCore> | null>(null);
|
||||
@@ -111,22 +103,6 @@ export function HistoryChartArea({
|
||||
|
||||
return (
|
||||
<div className="history-chart-shell" onWheelCapture={handleWheelCapture}>
|
||||
<div className="history-chart-controls" aria-label="Режим соединительной линии avg">
|
||||
<span>Линия avg</span>
|
||||
<div className="segmented history-chart-line-mode">
|
||||
{AVG_LINE_MODES.map((mode) => (
|
||||
<button
|
||||
key={mode.value}
|
||||
type="button"
|
||||
className={mode.value === avgLineMode ? 'history-chart-line-mode__button--active' : undefined}
|
||||
onClick={() => onAvgLineModeChange(mode.value)}
|
||||
aria-pressed={mode.value === avgLineMode}
|
||||
>
|
||||
{mode.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<ReactEChartsCore
|
||||
key={avgLineMode}
|
||||
ref={chartRef}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { AvgLineMode } from './chartTypes';
|
||||
|
||||
type HistoryChartAvgLineModeControlProps = {
|
||||
value: AvgLineMode;
|
||||
onChange: (mode: AvgLineMode) => void;
|
||||
};
|
||||
|
||||
const AVG_LINE_MODES: Array<{ value: AvgLineMode; label: string }> = [
|
||||
{ value: 'auto', label: 'Авто' },
|
||||
{ value: 'show', label: 'Рисовать' },
|
||||
{ value: 'hide', label: 'Не рисовать' },
|
||||
];
|
||||
|
||||
export function HistoryChartAvgLineModeControl({ value, onChange }: HistoryChartAvgLineModeControlProps) {
|
||||
return (
|
||||
<div className="archive-avgline-mode-control" aria-label="Режим соединительной линии avg">
|
||||
<span>Линия avg</span>
|
||||
<div className="segmented history-chart-line-mode">
|
||||
{AVG_LINE_MODES.map((mode) => (
|
||||
<button
|
||||
key={mode.value}
|
||||
type="button"
|
||||
className={mode.value === value ? 'history-chart-line-mode__button--active' : undefined}
|
||||
onClick={() => onChange(mode.value)}
|
||||
aria-pressed={mode.value === value}
|
||||
>
|
||||
{mode.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -41,7 +41,7 @@ function createAvgLineData(points: HistoryPoint[], granulate: string, breakOnGap
|
||||
}
|
||||
|
||||
const previousTime = new Date(finitePoints[index - 1].time).getTime();
|
||||
const hasAcceptableGap = currentTime - previousTime <= slotMs * 100;
|
||||
const hasAcceptableGap = currentTime - previousTime <= slotMs * 24;
|
||||
|
||||
return hasAcceptableGap ? [currentPoint] : [null, currentPoint];
|
||||
});
|
||||
|
||||
@@ -28,16 +28,41 @@
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.archive-avgline-mode-control .history-chart-line-mode button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.archive-avgline-mode-control .history-chart-line-mode button:first-child {
|
||||
border-top-left-radius: var(--radius);
|
||||
border-bottom-left-radius: var(--radius);
|
||||
}
|
||||
|
||||
.archive-avgline-mode-control .history-chart-line-mode button:last-child {
|
||||
border-top-right-radius: var(--radius);
|
||||
border-bottom-right-radius: var(--radius);
|
||||
}
|
||||
|
||||
.segmented--range-preset button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.segmented--range-preset button:first-child {
|
||||
border-top-left-radius: var(--radius);
|
||||
border-bottom-left-radius: var(--radius);
|
||||
}
|
||||
|
||||
.segmented--range-preset button:last-child {
|
||||
border-top-right-radius: var(--radius);
|
||||
border-bottom-right-radius: var(--radius);
|
||||
}
|
||||
|
||||
.history-chart-line-mode button {
|
||||
min-height: 24px;
|
||||
padding: 0 8px;
|
||||
font-size: 12px;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.history-chart-line-mode__button--active {
|
||||
color: #f8fafc;
|
||||
background: rgba(212, 165, 116, 0.16);
|
||||
border-color: rgba(212, 165, 116, 0.38);
|
||||
border-color: var(--line-strong);
|
||||
background: rgba(201, 122, 61, 0.18);
|
||||
}
|
||||
|
||||
.chart-section--archive .history-chart-shell,
|
||||
|
||||
@@ -216,6 +216,14 @@
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
|
||||
.archive-avgline-mode-control {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--muted);
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
|
||||
.archive-date-input {
|
||||
color-scheme: dark;
|
||||
min-height: 36px;
|
||||
|
||||
Reference in New Issue
Block a user