@@ -7,6 +7,8 @@ import { renderNumberLabels } from '@/lib/charts';
77import { getThemeColors } from '@/lib/colors' ;
88import { formatDate , DATE_FORMATS } from '@/lib/date' ;
99import { formatLongCurrency , formatLongNumber } from '@/lib/format' ;
10+ import dayjs from 'dayjs' ;
11+ import type { ManipulateType } from 'dayjs' ;
1012
1113const 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+
35155export 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