@@ -412,37 +412,115 @@ Note:
412
412
rollPeriod: timeWindow === ' daily-avg' ? 7 : 1 ,
413
413
labels: [' Date' , ` ${ countType === ' unique' ? ' Unique IPs' : ' Total Checks' } ` ],
414
414
ylabel: yLabel,
415
- title: ` ${ chartTitle} - ${ displayTimeWindow} ${ countType === ' unique' ? ' Unique' : ' Total' } Statistics`
415
+ title: ` ${ chartTitle} - ${ displayTimeWindow} ${ countType === ' unique' ? ' Unique' : ' Total' } Statistics` ,
416
+ axes: {x: {}}
416
417
};
417
418
418
- // Set X-axis formatting based on time window
419
- if (timeWindow === ' yearly' ) {
420
- chartConfig .axes = {
421
- x: {
422
- axisLabelFormatter : function (d ) {
423
- return d .getFullYear ().toString ();
424
- },
425
- ticker : function (a , b , pixels , opts , dygraph , vals ) {
426
- // Generate yearly ticks
427
- const startYear = new Date (a).getFullYear ();
428
- const endYear = new Date (b).getFullYear ();
429
- const ticks = [];
430
- for (let year = startYear; year <= endYear; year++ ) {
431
- ticks .push ({v: new Date (year, 0 , 1 ).getTime (), label: year .toString ()});
432
- }
433
- return ticks;
419
+ // Calculate X-axis labels dynamically whenever chart is rendered
420
+ chartConfig .axes .x .ticker = function (a , b , pixels , opts , dygraph , vals ) {
421
+ function offDayBoundary (d ) {
422
+ return d .getHours () > 0 || d .getMinutes () > 0 ||
423
+ d .getSeconds () > 0 || d .getMilliseconds () > 0 ;
424
+ }
425
+
426
+ const startDate = new Date (a);
427
+ const endDate = new Date (b);
428
+
429
+ // Clamp the date range to the closest boundaries within the range
430
+ switch (timeWindow) {
431
+ case ' yearly' :
432
+ // Round inward to the nearest year boundaries
433
+ if (startDate .getMonth () > 0 || startDate .getDate () > 1 || offDayBoundary (startDate)) {
434
+ startDate .setFullYear (startDate .getFullYear () + 1 , 0 , 1 );
435
+ startDate .setHours (0 , 0 , 0 , 0 );
434
436
}
435
- }
436
- };
437
- } else if (timeWindow === ' monthly' ) {
438
- chartConfig .axes = {
439
- x: {
440
- axisLabelFormatter : function (d ) {
441
- return d .getFullYear () + ' -' + String (d .getMonth () + 1 ).padStart (2 , ' 0' );
437
+ // Set to beginning of the final year
438
+ endDate .setMonth (0 , 1 );
439
+ endDate .setHours (0 , 0 , 0 , 0 );
440
+ break ;
441
+ case ' monthly' :
442
+ // Round inward to the nearest month boundaries
443
+ if (startDate .getDate () > 1 || offDayBoundary (startDate)) {
444
+ startDate .setMonth (startDate .getMonth () + 1 , 1 );
445
+ startDate .setHours (0 , 0 , 0 , 0 );
442
446
}
443
- }
447
+ // Set to beginning of the final month
448
+ endDate .setDate (1 );
449
+ endDate .setHours (0 , 0 , 0 , 0 );
450
+ break ;
451
+ default :
452
+ // Round inward to the nearest day boundaries
453
+ if (offDayBoundary (startDate)) {
454
+ startDate .setDate (startDate .getDate () + 1 );
455
+ startDate .setHours (0 , 0 , 0 , 0 );
456
+ }
457
+ // Set to beginning of the final day
458
+ endDate .setHours (0 , 0 , 0 , 0 );
459
+ }
460
+ // Note: If the raw startDate and endDate are timestamps less than
461
+ // 24 hours apart on the same day, which happens e.g. when the user
462
+ // zooms very far into the graph within a single day's time interval,
463
+ // then the rounded-later startDate will end up being later than the
464
+ // rounded-earlier endDate, and there won't be any ticks, and therefore
465
+ // no axis labels. But that is indeed the correct behavior, assuming we
466
+ // don't want to label the axis anywhere apart from on date boundaries.
467
+ // We could bend over backwards to do such custom labeling only in this
468
+ // case, but it's more code for an unimportant edge case: there are not
469
+ // actually any samples to be inspected inside a single day's interval.
470
+
471
+ const minPixelsPerTick =
472
+ timeWindow === ' yearly' ? 50 :
473
+ timeWindow === ' monthly' ? 75 : 100 ;
474
+ const numTicks = Math .max (2 , Math .floor (pixels / minPixelsPerTick));
475
+ const currentDate = new Date (startDate);
476
+ const yearStep = Math .ceil ((endDate .getFullYear () - startDate .getFullYear () + 1 ) / numTicks);
477
+ const startMonth = 12 * startDate .getFullYear () + startDate .getMonth ();
478
+ const endMonth = 12 * endDate .getFullYear () + endDate .getMonth ();
479
+ const monthStep = Math .ceil ((endMonth - startMonth + 1 ) / numTicks);
480
+ const msStep = Math .ceil ((endDate .getTime () - startDate .getTime () + 1 ) / numTicks);
481
+ const msPerDay = 1000 * 60 * 60 * 24 ;
482
+ const dayStep = Math .ceil (msStep / msPerDay);
483
+
484
+ function incrementDate (d , yearStep , monthStep , dayStep ) {
485
+ if (timeWindow === ' yearly' ) d .setFullYear (d .getFullYear () + yearStep);
486
+ else if (timeWindow === ' monthly' ) d .setMonth (d .getMonth () + monthStep);
487
+ else d .setDate (d .getDate () + dayStep);
488
+ }
489
+
490
+ const ticks = [];
491
+ while (ticks .length < numTicks && currentDate <= endDate) {
492
+ ticks .push ({
493
+ v: currentDate .getTime (),
494
+ label: opts (' axisLabelFormatter' ).call (dygraph, currentDate, 0 , opts, dygraph)
495
+ });
496
+ incrementDate (currentDate, yearStep, monthStep, dayStep);
497
+ }
498
+
499
+ return ticks;
500
+ };
501
+
502
+ // Format X-axis labels appropriately
503
+ const xLabelPre = ' <span style="font-size: 0.9em; white-space: nowrap;">' ;
504
+ const xLabelPost = ' </span>' ;
505
+ function xLabelYear (d ) { return d .getFullYear ().toString (); }
506
+ function xLabelMonth (d ) { return String (d .getMonth () + 1 ).padStart (2 , ' 0' ); }
507
+ function xLabelDay (d ) { return String (d .getDate ()).padStart (2 , ' 0' ); }
508
+ if (timeWindow === ' yearly' ) {
509
+ chartConfig .axes .x .axisLabelFormatter = function (d ) {
510
+ return ` ${ xLabelPre}${ xLabelYear (d)}${ xLabelPost} ` ;
444
511
};
445
512
}
513
+ else if (timeWindow === ' monthly' ) {
514
+ chartConfig .axes .x .axisLabelFormatter = function (d ) {
515
+ return ` ${ xLabelPre}${ xLabelYear (d)} -${ xLabelMonth (d)}${ xLabelPost} ` ;
516
+ };
517
+ }
518
+ else {
519
+ chartConfig .axes .x .axisLabelFormatter = function (d ) {
520
+ return ` ${ xLabelPre}${ xLabelYear (d)} -${ xLabelMonth (d)} -${ xLabelDay (d)}${ xLabelPost} ` ;
521
+ };
522
+ }
523
+
446
524
447
525
// If comparison mode is enabled and site2 is selected
448
526
if (op && site2 && site2 !== site) {
0 commit comments