Keep the theme redraw from wiping a chart's x scale

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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SKL7sUgNWhukDEz95vSMQv
This commit is contained in:
2026-08-20 14:33:17 +02:00
co-authored by Claude Opus 5
parent a74fe70de7
commit dfe7aae4d2
3 changed files with 28 additions and 7 deletions
+20 -5
View File
@@ -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({
<div className="relative min-h-0 flex-1">
<div ref={host} className="absolute inset-0" />
{points === 0 ? (
<p className="absolute inset-0 flex items-center justify-center text-sm text-muted-foreground">
{empty}
</p>
pending ? (
<Skeleton className="absolute inset-0" />
) : (
<p className="absolute inset-0 flex items-center justify-center text-sm text-muted-foreground">
{empty}
</p>
)
) : null}
</div>
{/* Kept at the legend's resting height, so the plot does not resize
@@ -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}
/>
<Chart
@@ -277,6 +283,7 @@ export function HealthActivity({ range }: { range: Range }) {
at((point) => point.avg_ms),
at((point) => point.avg_lag_ms),
]}
pending={seriesPending}
moment={failuresAt}
/>
</section>