Skip to content

Commit 72a6883

Browse files
committed
update-sites/stats: improve the X axis labels
1 parent a8dbb8d commit 72a6883

File tree

1 file changed

+103
-25
lines changed

1 file changed

+103
-25
lines changed

_pages/update-sites/stats.md

Lines changed: 103 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -412,37 +412,115 @@ Note:
412412
rollPeriod: timeWindow === 'daily-avg' ? 7 : 1,
413413
labels: ['Date', `${countType === 'unique' ? 'Unique IPs' : 'Total Checks'}`],
414414
ylabel: yLabel,
415-
title: `${chartTitle} - ${displayTimeWindow} ${countType === 'unique' ? 'Unique' : 'Total'} Statistics`
415+
title: `${chartTitle} - ${displayTimeWindow} ${countType === 'unique' ? 'Unique' : 'Total'} Statistics`,
416+
axes: {x: {}}
416417
};
417418

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);
434436
}
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);
442446
}
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}`;
444511
};
445512
}
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+
446524

447525
// If comparison mode is enabled and site2 is selected
448526
if (op && site2 && site2 !== site) {

0 commit comments

Comments
 (0)