From 3e95842b5fae9ee59b07596998a7e6d3045c9694 Mon Sep 17 00:00:00 2001 From: stroblme Date: Thu, 20 Aug 2026 14:33:17 +0200 Subject: [PATCH] Keep the theme redraw from wiping a chart's x scale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reaching Home from another page builds its charts in the same commit their theme effect first fires in, with the readings already cached. uPlot ranges its scales in a microtask, so `redraw()` re-set the x scale from the chart's own — still empty — bounds before that ran, and the pending range taken from the data was lost: axes without ticks, no lines, and no way back but the range control, which rebuilds the chart. Redrawing without the paths is all a colour swap needs and leaves the scales alone. The cards also carried "Nothing has run yet." while the first readings were still on their way; they now carry the skeleton the rest of Home uses. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01SKL7sUgNWhukDEz95vSMQv --- NOTEPAD.md | 1 - frontend/src/components/Common/UplotChart.tsx | 25 +++++++++++++++---- .../src/components/Health/HealthActivity.tsx | 9 ++++++- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/NOTEPAD.md b/NOTEPAD.md index c72d4b0..dcda05b 100644 --- a/NOTEPAD.md +++ b/NOTEPAD.md @@ -13,7 +13,6 @@ should reopen it. ### To be sorted -- BUG/UI the "Failures and Timing" and "Throughput per Minute" figures don't show any data upon intial load (and no loading indicator). Selecting a time range seems to trigger redraw and resolves it - BUG/UI when enlarging the code editor of a node, the code editor should enlarge to the left (node settings remain on the right) so that the code editor fills the center of the screen with the node properties available next to it - BUG/UX the console/log panel does not show print output of nodes - CHORE/UI: loop lag on Home reads a real number with no flows, and that is right — `LoopWatchdog` times how late `asyncio.sleep(1.0)` wakes on the API's event loop and is started unconditionally, so it measures the engine process rather than any flow, and it is what turns the health badge `degraded`. Nothing to fix; recorded so it is not reopened. diff --git a/frontend/src/components/Common/UplotChart.tsx b/frontend/src/components/Common/UplotChart.tsx index 7e43511..1169e7f 100644 --- a/frontend/src/components/Common/UplotChart.tsx +++ b/frontend/src/components/Common/UplotChart.tsx @@ -4,6 +4,7 @@ import "uplot/dist/uPlot.min.css" import type { HistoryPoint } from "@/client" import { useTheme } from "@/components/theme-provider" +import { Skeleton } from "@/components/ui/skeleton" import { si } from "@/lib/utils" /** @@ -74,6 +75,7 @@ export function UplotChart({ labels, plots, empty = "Nothing has come through yet.", + pending = false, unit, yRange, yLabel, @@ -85,6 +87,9 @@ export function UplotChart({ /** The points of each series, in the same order as `labels`. */ plots: HistoryPoint[][] empty?: string + /** No readings yet because none have arrived: a chart with nothing in it + * says "nothing has run", which is a different thing to say. */ + pending?: boolean /** Written after every reading, on the axis and in the legend. */ unit?: string /** A y axis fixed to these bounds; unset lets it follow the data. */ @@ -240,10 +245,16 @@ export function UplotChart({ chart.current.setData(table(plots)) }, [points, key]) - // The canvas cannot follow a CSS variable, so a theme swap is a redraw. + // The canvas cannot follow a CSS variable, so a theme swap is a redraw. The + // paths are geometry and stay as they are — and leaving them alone is what + // keeps this safe on the mount it also fires on: a full redraw re-sets the x + // scale from the chart's own bounds, which are still empty when the chart was + // built in this same commit (uPlot ranges its scales in a microtask). That + // overwrites the range it was about to take from the data, and the chart is + // left with no x axis and no lines until something sets its data again. // biome-ignore lint/correctness/useExhaustiveDependencies: the theme is the signal, not something the effect reads. useEffect(() => { - chart.current?.redraw() + chart.current?.redraw(false) }, [resolvedTheme]) return ( @@ -251,9 +262,13 @@ export function UplotChart({
{points === 0 ? ( -

- {empty} -

+ pending ? ( + + ) : ( +

+ {empty} +

+ ) ) : null}
{/* Kept at the legend's resting height, so the plot does not resize diff --git a/frontend/src/components/Health/HealthActivity.tsx b/frontend/src/components/Health/HealthActivity.tsx index 2695192..911b42e 100644 --- a/frontend/src/components/Health/HealthActivity.tsx +++ b/frontend/src/components/Health/HealthActivity.tsx @@ -100,11 +100,13 @@ function Chart({ title, labels, plots, + pending, moment, }: { title: string labels: string[] plots: HistoryPoint[][] + pending: boolean moment: Moment }) { return ( @@ -127,6 +129,7 @@ function Chart({ labels={labels} plots={plots} empty="Nothing has run yet." + pending={pending} onCursor={moment.onCursor} onSelect={moment.onSelect} /> @@ -205,7 +208,9 @@ export function HealthActivity({ range }: { range: Range }) { const runsAt = useMoment() const failuresAt = useMoment() - const { data: series } = useQuery(timeseriesQueryOptions(range)) + const { data: series, isPending: seriesPending } = useQuery( + timeseriesQueryOptions(range), + ) const { data: runs } = useQuery(runsQueryOptions(range)) const { data: pinnedRuns } = useQuery(minuteRunsQueryOptions(runsAt.pinned)) const { data: failures } = useQuery(failuresQueryOptions(range)) @@ -267,6 +272,7 @@ export function HealthActivity({ range }: { range: Range }) { at((point) => point.messages), at((point) => point.executions), ]} + pending={seriesPending} moment={runsAt} /> point.avg_ms), at((point) => point.avg_lag_ms), ]} + pending={seriesPending} moment={failuresAt} />