@@ -135,13 +135,16 @@ export class UIControlsRenderer extends Renderer {
135135 * @param {number } noOfDays - The number of days for the reporting range.
136136 * @returns {Array } The computed start and end dates of the reporting range.
137137 */
138+
138139 computeReportingRange ( noOfDays ) {
139- const finalDate = this . data [ this . data . length - 1 ] [ this . datePropertyName ] ;
140+ // Ensure finalDate is a Date object
141+ const finalDateRaw = this . data [ this . data . length - 1 ] [ this . datePropertyName ] ;
142+ const finalDate = finalDateRaw instanceof Date ? finalDateRaw : new Date ( finalDateRaw ) ;
140143 let endDate = new Date ( finalDate ) ;
141144 let startDate = addDaysToDate ( finalDate , - Number ( noOfDays ) ) ;
142145 if ( this . selectedTimeRange ) {
143- endDate = new Date ( this . selectedTimeRange [ 1 ] ) ;
144- startDate = new Date ( this . selectedTimeRange [ 0 ] ) ;
146+ endDate = this . selectedTimeRange [ 1 ] instanceof Date ? new Date ( this . selectedTimeRange [ 1 ] ) : new Date ( this . selectedTimeRange [ 1 ] ) ;
147+ startDate = this . selectedTimeRange [ 0 ] instanceof Date ? new Date ( this . selectedTimeRange [ 0 ] ) : new Date ( this . selectedTimeRange [ 0 ] ) ;
145148 const diffDays = Number ( noOfDays ) - calculateDaysBetweenDates ( startDate , endDate ) . roundedDays ;
146149 if ( diffDays < 0 ) {
147150 startDate = addDaysToDate ( startDate , - Number ( diffDays ) ) ;
@@ -154,8 +157,11 @@ export class UIControlsRenderer extends Renderer {
154157 }
155158 }
156159 }
157- if ( startDate < this . data [ 0 ] [ this . datePropertyName ] ) {
158- startDate = this . data [ 0 ] [ this . datePropertyName ] ;
160+ // Ensure startDate and endDate are not before/after data bounds
161+ const firstDateRaw = this . data [ 0 ] [ this . datePropertyName ] ;
162+ const firstDate = firstDateRaw instanceof Date ? firstDateRaw : new Date ( firstDateRaw ) ;
163+ if ( startDate < firstDate ) {
164+ startDate = firstDate ;
159165 }
160166 if ( endDate < this . x . domain ( ) [ 1 ] ) {
161167 endDate = this . x . domain ( ) [ 1 ] ;
@@ -173,20 +179,103 @@ export class UIControlsRenderer extends Renderer {
173179 createXAxis ( x , timeInterval = this . timeInterval ) {
174180 let axis ;
175181 switch ( timeInterval ) {
176- case 'days' :
182+ case 'days' : {
183+ const ticks = x . ticks ( d3 . timeDay . every ( 1 ) ) ;
177184 axis = d3
178185 . axisBottom ( x )
179- . ticks ( d3 . timeDay . every ( 1 ) ) // label every 2 days
186+ . ticks ( d3 . timeDay . every ( 1 ) )
180187 . tickFormat ( ( d , i ) => {
181- return i % 2 === 0 ? d3 . timeFormat ( '%b %d' ) ( d ) : '' ;
188+ const dayFormat = d3 . timeFormat ( '%b %d' ) ;
189+ const yearFormat = d3 . timeFormat ( '%Y' ) ;
190+ if ( i === 0 ) return `${ dayFormat ( d ) } ${ yearFormat ( d ) } ` ;
191+ return i % 2 === 0 ? dayFormat ( d ) : '' ;
182192 } ) ;
183193 break ;
184- case 'weeks' :
185- axis = d3 . axisBottom ( x ) . ticks ( d3 . timeWeek ) ;
194+ }
195+ case 'weeks' : {
196+ const ticks = x . ticks ( d3 . timeDay ) ;
197+ // Find the first tick that is a Monday
198+ let firstMonday = - 1 ;
199+ for ( let i = 0 ; i < ticks . length ; i ++ ) {
200+ if ( ticks [ i ] . getDay ( ) === 1 ) {
201+ firstMonday = i ;
202+ break ;
203+ }
204+ }
205+ axis = d3
206+ . axisBottom ( x )
207+ . ticks ( d3 . timeDay )
208+ . tickFormat ( ( d , i ) => {
209+ const dayFormat = d3 . timeFormat ( '%b %d' ) ;
210+ const yearFormat = d3 . timeFormat ( '%Y' ) ;
211+ if ( i === firstMonday ) return `${ dayFormat ( d ) } ${ yearFormat ( d ) } ` ;
212+ return d . getDay ( ) === 1 && i > firstMonday ? dayFormat ( d ) : '' ;
213+ } ) ;
186214 break ;
187- case 'months' :
188- axis = d3 . axisBottom ( x ) . ticks ( d3 . timeMonth ) ;
215+ }
216+ case 'months' : {
217+ const ticks = x . ticks ( d3 . timeWeek ) ;
218+ // Find the first tick that is the first week of a month
219+ let firstMonthWeek = - 1 ;
220+ for ( let i = 0 ; i < ticks . length ; i ++ ) {
221+ if ( ticks [ i ] . getDate ( ) <= 7 ) {
222+ firstMonthWeek = i ;
223+ break ;
224+ }
225+ }
226+ const weeks = d3 . timeWeek . range ( x . domain ( ) [ 0 ] , x . domain ( ) [ 1 ] ) ;
227+ axis = d3
228+ . axisBottom ( x )
229+ . ticks ( d3 . timeWeek )
230+ . tickFormat ( ( d , i ) => {
231+ const monthFormat = d3 . timeFormat ( '%b' ) ;
232+ const yearFormat = d3 . timeFormat ( '%Y' ) ;
233+ if ( i === firstMonthWeek ) return `${ monthFormat ( d ) } ${ yearFormat ( d ) } ` ;
234+ if ( d . getDate ( ) <= 7 && i > firstMonthWeek ) {
235+ if ( i > 0 && d . getFullYear ( ) !== weeks [ i - 1 ] . getFullYear ( ) ) {
236+ return `${ monthFormat ( d ) } ${ yearFormat ( d ) } ` ;
237+ }
238+ return monthFormat ( d ) ;
239+ }
240+ return '' ;
241+ } ) ;
189242 break ;
243+ }
244+ case 'bimonthly' : {
245+ const ticks = x . ticks ( d3 . timeMonth ) ;
246+ // Find the first tick that is the first month
247+ let firstQuarterMonth = - 1 ;
248+ for ( let i = 0 ; i < ticks . length ; i ++ ) {
249+ if ( ticks [ i ] . getMonth ( ) % 2 === 0 ) {
250+ firstQuarterMonth = i ;
251+ break ;
252+ }
253+ }
254+ const months = d3 . timeMonth . range ( x . domain ( ) [ 0 ] , x . domain ( ) [ 1 ] ) ;
255+ axis = d3
256+ . axisBottom ( x )
257+ . ticks ( d3 . timeMonth )
258+ . tickFormat ( ( d , i ) => {
259+ const monthFormat = d3 . timeFormat ( '%b' ) ;
260+ const yearFormat = d3 . timeFormat ( '%Y' ) ;
261+ if ( i === firstQuarterMonth ) return `${ monthFormat ( d ) } ${ yearFormat ( d ) } ` ;
262+ if ( d . getMonth ( ) % 2 === 0 && i > firstQuarterMonth ) {
263+ // Show year if year changes from previous quarter tick
264+ const prevQuarterIndex = ( ( ) => {
265+ for ( let j = i - 1 ; j >= 0 ; j -- ) {
266+ if ( months [ j ] . getMonth ( ) % 2 === 0 ) return j ;
267+ }
268+ return - 1 ;
269+ } ) ( ) ;
270+ if ( prevQuarterIndex >= 0 && d . getFullYear ( ) !== months [ prevQuarterIndex ] . getFullYear ( ) ) {
271+ return `${ monthFormat ( d ) } ${ yearFormat ( d ) } ` ;
272+ }
273+ return monthFormat ( d ) ;
274+ }
275+ return '' ;
276+ } ) ;
277+ break ;
278+ }
190279 default :
191280 return d3 . axisBottom ( x ) ;
192281 }
@@ -210,6 +299,9 @@ export class UIControlsRenderer extends Renderer {
210299 case 'days' :
211300 this . timeInterval = 'weeks' ;
212301 break ;
302+ case 'bimonthly' :
303+ this . timeInterval = 'weeks' ;
304+ break ;
213305 default :
214306 this . timeInterval = 'weeks' ;
215307 }
@@ -220,14 +312,17 @@ export class UIControlsRenderer extends Renderer {
220312 this . eventBus ?. emitEvents ( `change-time-interval-${ this . chartName } ` , this . timeInterval ) ;
221313 }
222314
223- determineTheAppropriateAxisLabels ( ) {
224- if ( this . reportingRangeDays <= 31 ) {
315+ determineTheAppropriateAxisLabels ( noOfDays = this . reportingRangeDays ) {
316+ if ( noOfDays <= 31 ) {
225317 return 'days' ;
226318 }
227- if ( this . reportingRangeDays > 31 && this . reportingRangeDays <= 124 ) {
319+ if ( noOfDays > 31 && noOfDays <= 150 ) {
228320 return 'weeks' ;
229321 }
230- return 'months' ;
322+ if ( noOfDays > 150 && noOfDays <= 750 ) {
323+ return 'months' ;
324+ }
325+ return 'bimonthly' ;
231326 }
232327
233328 /**
0 commit comments