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