From b13a35c310ad31898a3abda1c4cb00615615aa5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Thu, 26 Mar 2026 10:19:23 +0100 Subject: [PATCH] fix echart --- frontend/src/App.tsx | 33 +++++++++---- frontend/src/components/common/EChart.tsx | 4 +- .../src/components/common/EChart.tsx.bak2 | 49 +++++++++++++++++++ 3 files changed, 75 insertions(+), 11 deletions(-) create mode 100644 frontend/src/components/common/EChart.tsx.bak2 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e5dfbfe..906ea6e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -60,7 +60,6 @@ const STORAGE_KEYS = { liveWidgets: "pv-live-widgets-v4", liveMetrics: "pv-live-metrics-v4", archiveMetrics: "pv-archive-metrics-v4", - archiveAutoRefresh: "pv-archive-autorefresh-v1", liveAutoRefresh: "pv-live-autorefresh-v1", }; @@ -518,7 +517,6 @@ export default function App() { const [archiveRange, setArchiveRange] = useState("today"); const [liveMetrics, setLiveMetrics] = useState(() => readStorage(STORAGE_KEYS.liveMetrics, DEFAULT_LIVE_METRICS)); const [archiveMetrics, setArchiveMetrics] = useState(() => readStorage(STORAGE_KEYS.archiveMetrics, DEFAULT_LIVE_METRICS)); - const [archiveAutoRefresh, setArchiveAutoRefresh] = useState(() => readStorage(STORAGE_KEYS.archiveAutoRefresh, true, (raw) => raw === "true")); const [liveAutoRefresh, setLiveAutoRefresh] = useState(() => readStorage(STORAGE_KEYS.liveAutoRefresh, true, (raw) => raw === "true")); const [liveWidgets, setLiveWidgets] = useState(() => getVisibleLiveWidgets(readStorage(STORAGE_KEYS.liveWidgets, DEFAULT_LIVE_WIDGETS))); const [viewMode, setViewMode] = useState(() => { const fromUrl = parseViewModeFromLocation(); return fromUrl === "kiosk" ? fromUrl : readStorage(STORAGE_KEYS.viewMode, "normal", (raw) => (raw === "kiosk" ? "kiosk" : "normal")); }); @@ -572,7 +570,6 @@ export default function App() { useEffect(() => { writeStorage(STORAGE_KEYS.liveWidgets, liveWidgets); }, [liveWidgets]); useEffect(() => { writeStorage(STORAGE_KEYS.liveMetrics, liveMetrics); }, [liveMetrics]); useEffect(() => { writeStorage(STORAGE_KEYS.archiveMetrics, archiveMetrics); }, [archiveMetrics]); - useEffect(() => { writeStorage(STORAGE_KEYS.archiveAutoRefresh, String(archiveAutoRefresh)); }, [archiveAutoRefresh]); useEffect(() => { writeStorage(STORAGE_KEYS.liveAutoRefresh, String(liveAutoRefresh)); }, [liveAutoRefresh]); const dataEnabled = authenticated || authEnabled === false; @@ -632,12 +629,11 @@ export default function App() { const effectiveBucket = kioskActive ? effectiveKioskSettings.analytics_bucket : bucket; const effectiveCompare = kioskActive ? effectiveKioskSettings.compare_mode : compare; const historyQuery = useRealtimeHistory(effectiveRealtimeRange, dataEnabled, { publicKiosk: publicMode, pauseAutoRefresh: !liveAutoRefresh }); - const archiveManualRangeActive = Boolean(archiveStart && archiveEnd); const sanitizedCompareRanges = compareRanges.filter((item) => item.start && item.end); const analyticsOptions = analyticsStart && analyticsEnd && !kioskActive ? { start: analyticsStart, end: analyticsEnd, publicKiosk: publicMode, compareRanges: effectiveCompare === "custom_multi" ? sanitizedCompareRanges : undefined } : { publicKiosk: publicMode, compareRanges: effectiveCompare === "custom_multi" ? sanitizedCompareRanges : undefined }; const analyticsQuery = useAnalytics(analyticsStart && analyticsEnd && !kioskActive ? "custom" : effectiveAnalyticsRange, effectiveBucket, effectiveCompare, dataEnabled, analyticsOptions); const historical = useHistoricalImport(hasWarehouseAccess); - const archiveQuery = useRealtimeHistory(archiveStart && archiveEnd ? "custom" : archiveRange, dataEnabled, { start: archiveStart || undefined, end: archiveEnd || undefined, metrics: archiveMetrics, publicKiosk: publicMode, pauseAutoRefresh: !archiveAutoRefresh }); + const archiveQuery = useRealtimeHistory(archiveStart && archiveEnd ? "custom" : archiveRange, dataEnabled, { start: archiveStart || undefined, end: archiveEnd || undefined, metrics: archiveMetrics, publicKiosk: publicMode, pauseAutoRefresh: true }); const rawRealtimeHistoryData = useMemo(() => trimSingleDayHistory(historyQuery.data, effectiveRealtimeRange), [historyQuery.data, effectiveRealtimeRange]); const liveHistoryData = useMemo(() => filterHistoryByMetrics(rawRealtimeHistoryData, liveHistoryMetrics), [rawRealtimeHistoryData, liveHistoryMetrics]); const archiveHistoryData = useMemo(() => filterHistoryByMetrics(trimSingleDayHistory(archiveQuery.data, archiveRange), archiveMetrics), [archiveQuery.data, archiveRange, archiveMetrics]); @@ -793,9 +789,7 @@ export default function App() { const subnavRefreshControls = activeTab === "realtime" ?
- : activeTab === "archive" - ?
- : null; + : null; const menu = (
@@ -833,7 +827,7 @@ export default function App() {
{navbar}{menu}
{activeTab === "realtime" && <>
{effectiveLiveWidgets.map((widgetId) => renderLiveWidget(widgetId))}
} - {activeTab === "archive" && <>
{ setArchiveRange(value); applyArchivePreset(value, setArchiveStart, setArchiveEnd); setArchiveAutoRefresh(true); }} options={archiveQuickRangeOptions(language)} /> item.key === archiveRange) ? archiveRange : ""} onChange={(value) => { setArchiveRange(value); applyArchivePreset(value, setArchiveStart, setArchiveEnd); setArchiveAutoRefresh(true); }} options={[{ key: "", label: language === "en" ? "Choose range" : "Wybierz zakres" }, ...archiveListRangeOptions(language), { key: "custom", label: language === "en" ? "Custom range" : "Własny zakres" }]} />
{ setArchiveRange("custom"); setArchiveStart(e.target.value); setArchiveAutoRefresh(false); }} />
{ setArchiveRange("custom"); setArchiveEnd(e.target.value); setArchiveAutoRefresh(false); }} />
{archiveAutoRefresh && !archiveManualRangeActive ? (language === "en" ? "Auto refresh active" : "Auto-odświeżanie aktywne") : (language === "en" ? "Auto refresh paused" : "Auto-odświeżanie zatrzymane")}
{archiveManualRangeActive ? (language === "en" ? "Manual date range disables automatic refresh until you switch it back on." : "Ręczny zakres dat zatrzymuje auto-odświeżanie, dopóki nie włączysz go ponownie.") : (language === "en" ? "Use the picker below to freeze the chart on a chosen time window." : "Użyj pickera poniżej, aby zamrozić wykres na wybranym zakresie czasu.")}
} />
item.metric_id !== "energy_total")} selected={archiveMetrics.filter((item) => item !== "energy_total")} onChange={setArchiveMetrics} />
} + {activeTab === "archive" && <>
{ setArchiveRange(value); applyArchivePreset(value, setArchiveStart, setArchiveEnd); }} options={archiveQuickRangeOptions(language)} /> item.key === archiveRange) ? archiveRange : ""} onChange={(value) => { setArchiveRange(value); applyArchivePreset(value, setArchiveStart, setArchiveEnd); }} options={[{ key: "", label: language === "en" ? "Choose range" : "Wybierz zakres" }, ...archiveListRangeOptions(language), { key: "custom", label: language === "en" ? "Custom range" : "Własny zakres" }]} />
{ setArchiveRange("custom"); setArchiveStart(e.target.value); }} />
{ setArchiveRange("custom"); setArchiveEnd(e.target.value); }} />
{language === "en" ? "Refresh on change" : "Odświeżanie po zmianie"}
{language === "en" ? "This chart reloads only when you change the range or selected metrics." : "Ten wykres przeładowuje się tylko po zmianie zakresu albo wybranych metryk."}
} />
item.metric_id !== "energy_total")} selected={archiveMetrics.filter((item) => item !== "energy_total")} onChange={setArchiveMetrics} />
} {activeTab === "analytics" && <>
{ if (value !== "custom") { setAnalyticsRange(value); setAnalyticsStart(""); setAnalyticsEnd(""); } }} options={[...config.capabilities.ranges.filter((item) => !["6h", "24h", "48h", "1d", "3d", "14d", "60d", "ytd"].includes(item.key)).map((item) => ({ key: item.key, label: translateRangeLabel(language, item.key, item.label) })), { key: "custom", label: language === "en" ? "Custom" : "Ręczny" }]} /> ({ key: item.key, label: translateBucket(language, item.key) }))} />
{analyticsStart || analyticsEnd || (analyticsRange === "custom") || compare === "custom_multi" ?
setAnalyticsStart(e.target.value)} />
setAnalyticsEnd(e.target.value)} />
{compare === "custom_multi" ?
{language === "en" ? "Comparison ranges" : "Zakresy porównawcze"}
{compareRanges.map((item, index) =>
setCompareRanges(compareRanges.map((current) => current.key === item.key ? { ...current, label: e.target.value } : current))} />
setCompareRanges(compareRanges.map((current) => current.key === item.key ? { ...current, start: e.target.value } : current))} />
setCompareRanges(compareRanges.map((current) => current.key === item.key ? { ...current, end: e.target.value } : current))} />
)}
: null}
: null}
item.key === compare)?.label ?? compare} />
{allWidgets.production}
{allWidgets.comparison}
} @@ -879,7 +873,26 @@ function translateRangeLabel(language: Language, key: string, fallback: string): function HeroCards({ cards, locale, language }: { cards: SnapshotPayload["hero_cards"]; locale: string; language: Language }) { return
{cards.map((card) =>
{iconForMetric(card.metric_id)}{card.unit || "live"}
{labelForMetric(language, card.metric_id, card.label)}
{formatValue(card.value, card.unit, 2, locale)}
{card.subtitle}
)}
; } function QuickMetrics({ metrics, locale, language }: { metrics: MetricValue[]; locale: string; language: Language }) { return

{t(language, "quickMetrics")}

{metrics.map((metric) =>
{labelForMetric(language, metric.metric_id, metric.label)}
{metric.unit}
{formatValue(metric.value, metric.unit, 2, locale)}
)}
; } function StatusStat({ label, value }: { label: string; value: string }) { return
{label}
{value}
; } -function LiveHistoryPanel({ data, language, theme, title, subtitle, footer, freezeUpdates = false }: { data?: HistoryPayload; language: Language; theme: ThemeMode; title: string; subtitle: string; footer?: ReactNode; freezeUpdates?: boolean }) { const [displayData, setDisplayData] = useState(data); const latestIncomingRef = useRef(data); useEffect(() => { latestIncomingRef.current = data; if (!freezeUpdates) setDisplayData(data); }, [data, freezeUpdates]); const chartOption = useMemo(() => buildLiveHistoryOption(displayData, theme, language), [displayData, theme, language]); return

{title}

{subtitle}
{footer ?
{footer}
: null}
; } +function LiveHistoryPanel({ data, language, theme, title, subtitle, footer, freezeUpdates = false, chartKey }: { data?: HistoryPayload; language: Language; theme: ThemeMode; title: string; subtitle: string; footer?: ReactNode; freezeUpdates?: boolean; chartKey?: string }) { + const [displayData, setDisplayData] = useState(data); + + useEffect(() => { + if (!freezeUpdates) { + setDisplayData(data); + } + }, [data, freezeUpdates]); + + useEffect(() => { + if (!freezeUpdates && chartKey) { + setDisplayData(data); + } + }, [chartKey, data, freezeUpdates]); + + const resolvedData = freezeUpdates ? displayData : data; + const chartOption = useMemo(() => buildLiveHistoryOption(resolvedData, theme, language), [resolvedData, theme, language]); + + return

{title}

{subtitle}
{footer ?
{footer}
: null}
; +} function SystemStatus({ items, locale, language }: { items: MetricValue[]; locale: string; language: Language }) { return

{t(language, "systemStatus")}

{items.map((metric) =>
{labelForMetric(language, metric.metric_id, metric.label)}
{metric.unit || metric.status}
{formatValue(metric.value, metric.unit, 2, locale)}
)}
; } function StringPanels({ rows, locale, language }: { rows: SnapshotGroupRow[]; locale: string; language: Language }) { return

{t(language, "strings")}

{rows.map((row) =>
{row.label}
{Object.values(row.values).map((metric) =>
{labelForMetric(language, metric.metric_id, metric.label)}{formatValue(metric.value, metric.unit, 2, locale)}
)}
)}
; } function StatusDot({ ok }: { ok: boolean }) { return ; } diff --git a/frontend/src/components/common/EChart.tsx b/frontend/src/components/common/EChart.tsx index eab0de5..002e2bb 100644 --- a/frontend/src/components/common/EChart.tsx +++ b/frontend/src/components/common/EChart.tsx @@ -29,7 +29,9 @@ export function EChart({ option, className = "h-80 w-full" }: EChartProps) { return; } - chartRef.current.setOption(option, { notMerge: false, lazyUpdate: true }); + chartRef.current.clear(); + chartRef.current.setOption(option, { notMerge: true, lazyUpdate: false }); + chartRef.current.resize(); }, [option]); useEffect(() => { diff --git a/frontend/src/components/common/EChart.tsx.bak2 b/frontend/src/components/common/EChart.tsx.bak2 new file mode 100644 index 0000000..eab0de5 --- /dev/null +++ b/frontend/src/components/common/EChart.tsx.bak2 @@ -0,0 +1,49 @@ +import { useEffect, useRef } from "react"; +import * as echarts from "echarts"; +import type { EChartsOption } from "echarts"; + +interface EChartProps { + option: EChartsOption; + className?: string; +} + +export function EChart({ option, className = "h-80 w-full" }: EChartProps) { + const ref = useRef(null); + const chartRef = useRef(null); + + useEffect(() => { + if (!ref.current || chartRef.current) { + return; + } + + chartRef.current = echarts.init(ref.current); + + return () => { + chartRef.current?.dispose(); + chartRef.current = null; + }; + }, []); + + useEffect(() => { + if (!chartRef.current) { + return; + } + + chartRef.current.setOption(option, { notMerge: false, lazyUpdate: true }); + }, [option]); + + useEffect(() => { + if (!ref.current || !chartRef.current) { + return; + } + + const observer = new ResizeObserver(() => chartRef.current?.resize()); + observer.observe(ref.current); + + return () => { + observer.disconnect(); + }; + }, []); + + return
; +}