diff --git a/client/src/stats/DayChart.tsx b/client/src/stats/DayChart.tsx index 780369b..c71608d 100644 --- a/client/src/stats/DayChart.tsx +++ b/client/src/stats/DayChart.tsx @@ -32,7 +32,12 @@ export function DayChart({ labels, series }: { labels: string[]; series: ChartSe height: 220, legend: { show: series.length > 1 }, cursor: { drag: { x: false, y: false } }, - scales: { x: { time: false } }, + // distr: 2 (ordinal) — the default (1, linear) lets uPlot's tick + // generator pick fractional increments on a short/sparse series, + // and the label lookup below (`labels[v]`) misses on a non-integer + // v, rendering a blank tick. Ordinal forces whole-number splits, + // which is what an index-based x-axis always wants anyway (#55). + scales: { x: { time: false, distr: 2 } }, axes: [ { stroke: '#888', grid: { stroke: '#333' }, values: (_u, vals) => vals.map(v => labels[v] ?? '') }, { stroke: '#888', grid: { stroke: '#333' } }, diff --git a/client/src/stats/Stats.tsx b/client/src/stats/Stats.tsx index 6596c3b..4f16384 100644 --- a/client/src/stats/Stats.tsx +++ b/client/src/stats/Stats.tsx @@ -135,26 +135,40 @@ function Dashboard({ data, isAdmin, onReset }: { data: StatsData; isAdmin: boole // by_day/per_drink_by_day come pre-sorted from the server (by_day newest // first for the table below, per_drink_by_day oldest first for the // chart's left-to-right reading order) — no re-sort needed here. - const revenueSeries = useMemo( - () => [...data.by_day].reverse().map(d => ({ x: fmtDayShort(d.day), y: d.paid_cents / 100 })), - [data.by_day] - ); - const txSeries = useMemo( - () => [...data.by_day].reverse().map(d => ({ x: fmtDayShort(d.day), y: d.tx_count })), - [data.by_day] - ); + // + // Each of these produces the full {labels, series} shape DayChart wants, + // memoized as one object (#55) — building `series`/`labels` as fresh + // array/object literals inline in JSX on every render defeats DayChart's + // effect (keyed on those props by reference, not deep-equality), so + // Dashboard re-rendering for any unrelated reason (e.g. the isAdmin + // check resolving after data already loaded) would destroy/recreate + // every uPlot instance for no data-related reason. + const revenueChart = useMemo(() => { + const days = [...data.by_day].reverse(); + return { labels: days.map(d => fmtDayShort(d.day)), series: [{ name: 'Umsatz (€)', values: days.map(d => d.paid_cents / 100) }] }; + }, [data.by_day]); + const txChart = useMemo(() => { + const days = [...data.by_day].reverse(); + return { labels: days.map(d => fmtDayShort(d.day)), series: [{ name: 'Transaktionen', values: days.map(d => d.tx_count) }] }; + }, [data.by_day]); // #45's two hourly readings — see the by_hour/by_hour_of_day comment on // computeStats() server-side for why they're separate. Revenue only // here (not also a tx-count variant like the per-day charts above) to // keep the page from growing a chart per metric per granularity; ask if // you want tx-count broken out hourly too. - const hourlyRevenue = useMemo( - () => data.by_hour.map(h => ({ x: fmtHour(h.hour), y: h.paid_cents / 100 })), + const hourlyRevenueChart = useMemo( + () => ({ + labels: data.by_hour.map(h => fmtHour(h.hour)), + series: [{ name: 'Umsatz (€)', values: data.by_hour.map(h => h.paid_cents / 100) }], + }), [data.by_hour] ); - const hourOfDayRevenue = useMemo( - () => data.by_hour_of_day.map(h => ({ x: `${String(h.hour_of_day).padStart(2, '0')}h`, y: h.paid_cents / 100 })), + const hourOfDayRevenueChart = useMemo( + () => ({ + labels: data.by_hour_of_day.map(h => `${String(h.hour_of_day).padStart(2, '0')}h`), + series: [{ name: 'Umsatz (€)', values: data.by_hour_of_day.map(h => h.paid_cents / 100) }], + }), [data.by_hour_of_day] ); @@ -187,24 +201,24 @@ function Dashboard({ data, isAdmin, onReset }: { data: StatsData; isAdmin: boole
Noch keine Verkäufe
- :Noch keine Verkäufe
- :Noch keine Verkäufe
- :Noch keine Verkäufe
- :