Skip to content

Commit d1be7bc

Browse files
committed
fix(events): pad hourly buckets in Events 'Today' chart and use locale for labels
1 parent 03c892a commit d1be7bc

File tree

1 file changed

+130
-2
lines changed

1 file changed

+130
-2
lines changed

src/components/charts/BarChart.tsx

Lines changed: 130 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { renderNumberLabels } from '@/lib/charts';
77
import { getThemeColors } from '@/lib/colors';
88
import { formatDate, DATE_FORMATS } from '@/lib/date';
99
import { formatLongCurrency, formatLongNumber } from '@/lib/format';
10+
import dayjs from 'dayjs';
11+
import type { ManipulateType } from 'dayjs';
1012

1113
const dateFormats = {
1214
millisecond: 'T',
@@ -32,6 +34,124 @@ export interface BarChartProps extends ChartProps {
3234
maxDate?: Date;
3335
}
3436

37+
function stepByUnit(start: dayjs.Dayjs, end: dayjs.Dayjs, unit: ManipulateType) {
38+
const steps: dayjs.Dayjs[] = [];
39+
let cur = start.startOf(unit);
40+
const endBound = end.startOf(unit);
41+
while (cur.isBefore(endBound) || cur.isSame(endBound)) {
42+
steps.push(cur);
43+
cur = cur.add(1, unit);
44+
// safety guard
45+
if (steps.length > 10000) break;
46+
}
47+
return steps;
48+
}
49+
50+
/**
51+
* Pads time-series chartData between minDate..maxDate by unit.
52+
* Supports common chartData shapes:
53+
* 1) Chart.js style: { labels: string[], datasets: [{ label, data: number[] | {x,y}[] }] }
54+
* 2) Series style: [{ label, data: [{ x, y }] }]
55+
*/
56+
57+
function padTimeSeriesChartData(
58+
chartData: any,
59+
unit: ManipulateType,
60+
minDate?: Date,
61+
maxDate?: Date,
62+
) {
63+
if (!unit || !minDate || !maxDate || !chartData) return chartData;
64+
65+
const start = dayjs(minDate);
66+
const end = dayjs(maxDate);
67+
68+
// build the canonical list of step timestamps (ISO strings)
69+
const steps = stepByUnit(start, end, unit);
70+
const stepKeys = steps.map(s => s.toISOString());
71+
72+
// helper to find value by x in an array of {x,y}
73+
const mapArrayXY = (arr: any[]) => {
74+
const m = new Map<string, number>();
75+
arr.forEach(d => {
76+
if (!d) return;
77+
const x = d.x ? dayjs(d.x).toISOString() : d[0] ? dayjs(d[0]).toISOString() : null;
78+
const y =
79+
typeof d.y === 'number' ? d.y : Array.isArray(d) && typeof d[1] === 'number' ? d[1] : 0;
80+
if (x) {
81+
// accumulate if duplicates exist
82+
m.set(x, (m.get(x) || 0) + (y || 0));
83+
}
84+
});
85+
return m;
86+
};
87+
88+
// Case A: Chart.js style
89+
if (chartData && chartData.labels && Array.isArray(chartData.datasets)) {
90+
// Normalize: if dataset.data is array of numbers aligned with labels -> create label->value map
91+
const newLabels = stepKeys.map(k => formatDate(new Date(k), DATE_FORMATS[unit], 'en')); // labels formatted; locale handled by Chart options
92+
const newDatasets = chartData.datasets.map((ds: any) => {
93+
// two subcases: ds.data is array of primitives aligning to chartData.labels OR array of {x,y}
94+
if (!ds || !Array.isArray(ds.data)) return { ...ds, data: Array(newLabels.length).fill(0) };
95+
96+
// detect object entries
97+
const first = ds.data[0];
98+
if (first && typeof first === 'object' && ('x' in first || 'y' in first)) {
99+
const m = mapArrayXY(ds.data);
100+
const data = stepKeys.map(k => m.get(k) || 0);
101+
return { ...ds, data };
102+
}
103+
104+
// otherwise assume ds.data aligns with chartData.labels
105+
const labelMap = new Map<string, number>();
106+
(chartData.labels || []).forEach((lbl: any, idx: number) => {
107+
const key = (lbl && new Date(lbl).toISOString()) || lbl; // try to convert label -> ISO if possible
108+
labelMap.set(key, ds.data[idx] ?? 0);
109+
// also store raw label string
110+
labelMap.set(String(lbl), ds.data[idx] ?? 0);
111+
});
112+
113+
const data = stepKeys.map(k => labelMap.get(k) ?? labelMap.get(new Date(k).toString()) ?? 0);
114+
return { ...ds, data };
115+
});
116+
117+
return { ...chartData, labels: newLabels, datasets: newDatasets };
118+
}
119+
120+
// Case A2: Chart.js-style object with datasets but without labels,
121+
// where datasets[].data is [{ x, y }] (this is the shape EventsChart produces)
122+
if (chartData && Array.isArray(chartData.datasets) && !chartData.labels) {
123+
const newDatasets = chartData.datasets.map((ds: any) => {
124+
if (!ds || !Array.isArray(ds.data)) {
125+
// produce zero series aligned to steps
126+
const data = stepKeys.map(k => ({ x: k, y: 0 }));
127+
return { ...ds, data };
128+
}
129+
const m = mapArrayXY(ds.data);
130+
const data = stepKeys.map(k => ({ x: k, y: m.get(k) || 0 }));
131+
return { ...ds, data };
132+
});
133+
134+
// keep any other fields (like focusLabel) intact
135+
return { ...chartData, datasets: newDatasets };
136+
}
137+
138+
// Case B: Series style: array of series objects { label, data: [{ x, y }] }
139+
if (Array.isArray(chartData)) {
140+
const paddedSeries = chartData.map(series => {
141+
if (!series || !Array.isArray(series.data)) return { ...series, data: stepKeys.map(() => 0) };
142+
const m = mapArrayXY(series.data);
143+
// produce data array aligned with steps (each element { x: <iso>, y: <num> } or number depending on original)
144+
// We'll return in the { x, y } form so Chart can understand timeseries data
145+
const data = stepKeys.map(k => ({ x: k, y: m.get(k) || 0 }));
146+
return { ...series, data };
147+
});
148+
return paddedSeries;
149+
}
150+
151+
// fallback: return original
152+
return chartData;
153+
}
154+
35155
export function BarChart({
36156
chartData,
37157
renderXLabel,
@@ -50,6 +170,14 @@ export function BarChart({
50170
const { locale } = useLocale();
51171
const { colors } = useMemo(() => getThemeColors(theme), [theme]);
52172

173+
// If this is a timeseries and we have min/max and a time unit, pad missing steps
174+
const paddedChartData = useMemo(() => {
175+
if (XAxisType === 'timeseries' && unit && minDate && maxDate) {
176+
return padTimeSeriesChartData(chartData, unit as ManipulateType, minDate, maxDate);
177+
}
178+
return chartData;
179+
}, [chartData, unit, XAxisType, minDate?.toString(), maxDate?.toString()]);
180+
53181
const chartOptions: any = useMemo(() => {
54182
return {
55183
__id: new Date().getTime(),
@@ -94,7 +222,7 @@ export function BarChart({
94222
},
95223
},
96224
};
97-
}, [chartData, colors, unit, stacked, renderXLabel, renderYLabel]);
225+
}, [paddedChartData, colors, unit, stacked, renderXLabel, renderYLabel]);
98226

99227
const handleTooltip = ({ tooltip }: { tooltip: any }) => {
100228
const { opacity, labelColors, dataPoints } = tooltip;
@@ -121,7 +249,7 @@ export function BarChart({
121249
<Chart
122250
{...props}
123251
type="bar"
124-
chartData={chartData}
252+
chartData={paddedChartData}
125253
chartOptions={chartOptions}
126254
onTooltip={handleTooltip}
127255
/>

0 commit comments

Comments
 (0)