@@ -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,102 @@ export class UIControlsRenderer extends Renderer {
173179 createXAxis ( x , timeInterval = this . timeInterval ) {
174180 let axis ;
175181 switch ( timeInterval ) {
176- case 'days' :
182+ case 'days' : {
177183 axis = d3
178184 . axisBottom ( x )
179- . ticks ( d3 . timeDay . every ( 1 ) ) // label every 2 days
185+ . ticks ( d3 . timeDay . every ( 1 ) )
180186 . tickFormat ( ( d , i ) => {
181- return i % 2 === 0 ? d3 . timeFormat ( '%b %d' ) ( d ) : '' ;
187+ const dayFormat = d3 . timeFormat ( '%b %d' ) ;
188+ const yearFormat = d3 . timeFormat ( '%Y' ) ;
189+ if ( i === 0 ) return `${ dayFormat ( d ) } ${ yearFormat ( d ) } ` ;
190+ return i % 2 === 0 ? dayFormat ( d ) : '' ;
182191 } ) ;
183192 break ;
184- case 'weeks' :
185- axis = d3 . axisBottom ( x ) . ticks ( d3 . timeWeek ) ;
193+ }
194+ case 'weeks' : {
195+ const ticks = x . ticks ( d3 . timeDay ) ;
196+ // Find the first tick that is a Monday
197+ let firstMonday = - 1 ;
198+ for ( let i = 0 ; i < ticks . length ; i ++ ) {
199+ if ( ticks [ i ] . getDay ( ) === 1 ) {
200+ firstMonday = i ;
201+ break ;
202+ }
203+ }
204+ axis = d3
205+ . axisBottom ( x )
206+ . ticks ( d3 . timeDay )
207+ . tickFormat ( ( d , i ) => {
208+ const dayFormat = d3 . timeFormat ( '%b %d' ) ;
209+ const yearFormat = d3 . timeFormat ( '%Y' ) ;
210+ if ( i === firstMonday ) return `${ dayFormat ( d ) } ${ yearFormat ( d ) } ` ;
211+ return d . getDay ( ) === 1 && i > firstMonday ? dayFormat ( d ) : '' ;
212+ } ) ;
186213 break ;
187- case 'months' :
188- axis = d3 . axisBottom ( x ) . ticks ( d3 . timeMonth ) ;
214+ }
215+ case 'months' : {
216+ const ticks = x . ticks ( d3 . timeWeek ) ;
217+ // Find the first tick that is the first week of a month
218+ let firstMonthWeek = - 1 ;
219+ for ( let i = 0 ; i < ticks . length ; i ++ ) {
220+ if ( ticks [ i ] . getDate ( ) <= 7 ) {
221+ firstMonthWeek = i ;
222+ break ;
223+ }
224+ }
225+ const weeks = d3 . timeWeek . range ( x . domain ( ) [ 0 ] , x . domain ( ) [ 1 ] ) ;
226+ axis = d3
227+ . axisBottom ( x )
228+ . ticks ( d3 . timeWeek )
229+ . tickFormat ( ( d , i ) => {
230+ const monthFormat = d3 . timeFormat ( '%b' ) ;
231+ const yearFormat = d3 . timeFormat ( '%Y' ) ;
232+ if ( i === firstMonthWeek ) return `${ monthFormat ( d ) } ${ yearFormat ( d ) } ` ;
233+ if ( d . getDate ( ) <= 7 && i > firstMonthWeek ) {
234+ if ( i > 0 && d . getFullYear ( ) !== weeks [ i - 1 ] . getFullYear ( ) ) {
235+ return `${ monthFormat ( d ) } ${ yearFormat ( d ) } ` ;
236+ }
237+ return monthFormat ( d ) ;
238+ }
239+ return '' ;
240+ } ) ;
189241 break ;
242+ }
243+ case 'bimonthly' : {
244+ const ticks = x . ticks ( d3 . timeMonth ) ;
245+ // Find the first tick that is the first month
246+ let firstQuarterMonth = - 1 ;
247+ for ( let i = 0 ; i < ticks . length ; i ++ ) {
248+ if ( ticks [ i ] . getMonth ( ) % 2 === 0 ) {
249+ firstQuarterMonth = i ;
250+ break ;
251+ }
252+ }
253+ const months = d3 . timeMonth . range ( x . domain ( ) [ 0 ] , x . domain ( ) [ 1 ] ) ;
254+ axis = d3
255+ . axisBottom ( x )
256+ . ticks ( d3 . timeMonth )
257+ . tickFormat ( ( d , i ) => {
258+ const monthFormat = d3 . timeFormat ( '%b' ) ;
259+ const yearFormat = d3 . timeFormat ( '%Y' ) ;
260+ if ( i === firstQuarterMonth ) return `${ monthFormat ( d ) } ${ yearFormat ( d ) } ` ;
261+ if ( d . getMonth ( ) % 2 === 0 && i > firstQuarterMonth ) {
262+ // Show year if year changes from previous quarter tick
263+ const prevQuarterIndex = ( ( ) => {
264+ for ( let j = i - 1 ; j >= 0 ; j -- ) {
265+ if ( months [ j ] . getMonth ( ) % 2 === 0 ) return j ;
266+ }
267+ return - 1 ;
268+ } ) ( ) ;
269+ if ( prevQuarterIndex >= 0 && d . getFullYear ( ) !== months [ prevQuarterIndex ] . getFullYear ( ) ) {
270+ return `${ monthFormat ( d ) } ${ yearFormat ( d ) } ` ;
271+ }
272+ return monthFormat ( d ) ;
273+ }
274+ return '' ;
275+ } ) ;
276+ break ;
277+ }
190278 default :
191279 return d3 . axisBottom ( x ) ;
192280 }
@@ -210,6 +298,9 @@ export class UIControlsRenderer extends Renderer {
210298 case 'days' :
211299 this . timeInterval = 'weeks' ;
212300 break ;
301+ case 'bimonthly' :
302+ this . timeInterval = 'weeks' ;
303+ break ;
213304 default :
214305 this . timeInterval = 'weeks' ;
215306 }
@@ -220,14 +311,17 @@ export class UIControlsRenderer extends Renderer {
220311 this . eventBus ?. emitEvents ( `change-time-interval-${ this . chartName } ` , this . timeInterval ) ;
221312 }
222313
223- determineTheAppropriateAxisLabels ( ) {
224- if ( this . reportingRangeDays <= 31 ) {
314+ determineTheAppropriateAxisLabels ( noOfDays = this . reportingRangeDays ) {
315+ if ( noOfDays <= 31 ) {
225316 return 'days' ;
226317 }
227- if ( this . reportingRangeDays > 31 && this . reportingRangeDays <= 124 ) {
318+ if ( noOfDays > 31 && noOfDays <= 150 ) {
228319 return 'weeks' ;
229320 }
230- return 'months' ;
321+ if ( noOfDays > 150 && noOfDays <= 750 ) {
322+ return 'months' ;
323+ }
324+ return 'bimonthly' ;
231325 }
232326
233327 /**
0 commit comments