@d3fc/d3fc-discontinuous-scale
Version:
A D3 scale that supports domains that are not continuous, allowing 'breaks' in the scale, for example skipping weekends for a date scale.
119 lines (95 loc) • 4.34 kB
JavaScript
import { timeDay, timeSaturday, timeMonday } from 'd3-time';
export const base = (dayAccessor, intervalDay, intervalSaturday, intervalMonday) => {
// the indices returned by dayAccessor(date)
const day = {
sunday: 0,
monday: 1,
saturday: 6
};
const millisPerDay = 24 * 3600 * 1000;
const millisPerWorkWeek = millisPerDay * 5;
const millisPerWeek = millisPerDay * 7;
const skipWeekends = {};
const isWeekend = (date) =>
dayAccessor(date) === 0 || dayAccessor(date) === 6;
skipWeekends.clampDown = (date) => {
if (date && isWeekend(date)) {
// round the date up to midnight
const newDate = intervalDay.ceil(date);
// then subtract the required number of days
if (dayAccessor(newDate) === day.sunday) {
return intervalDay.offset(newDate, -1);
} else if (dayAccessor(newDate) === day.monday) {
return intervalDay.offset(newDate, -2);
} else {
return newDate;
}
} else {
return date;
}
};
skipWeekends.clampUp = (date) => {
if (date && isWeekend(date)) {
// round the date down to midnight
const newDate = intervalDay.floor(date);
// then add the required number of days
if (dayAccessor(newDate) === day.saturday) {
return intervalDay.offset(newDate, 2);
} else if (dayAccessor(newDate) === day.sunday) {
return intervalDay.offset(newDate, 1);
} else {
return newDate;
}
} else {
return date;
}
};
// returns the number of included milliseconds (i.e. those which do not fall)
// within discontinuities, along this scale
skipWeekends.distance = function (startDate, endDate) {
startDate = skipWeekends.clampUp(startDate);
endDate = skipWeekends.clampDown(endDate);
// move the start date to the end of week boundary
const offsetStart = intervalSaturday.ceil(startDate);
if (endDate < offsetStart) {
return endDate.getTime() - startDate.getTime();
}
const msAdded = offsetStart.getTime() - startDate.getTime();
// move the end date to the end of week boundary
const offsetEnd = intervalSaturday.ceil(endDate);
const msRemoved = offsetEnd.getTime() - endDate.getTime();
// determine how many weeks there are between these two dates
// round to account for DST transitions
const weeks = Math.round((offsetEnd.getTime() - offsetStart.getTime()) / millisPerWeek);
return weeks * millisPerWorkWeek + msAdded - msRemoved;
};
skipWeekends.offset = function (startDate, ms) {
let date = isWeekend(startDate) ? skipWeekends.clampUp(startDate) : startDate;
if (ms === 0) {
return date;
}
const isNegativeOffset = ms < 0;
const isPositiveOffset = ms > 0;
let remainingms = ms;
// move to the end of week boundary for a postive offset or to the start of a week for a negative offset
const weekBoundary = isNegativeOffset ? intervalMonday.floor(date) : intervalSaturday.ceil(date);
remainingms -= (weekBoundary.getTime() - date.getTime());
// if the distance to the boundary is greater than the number of ms
// simply add the ms to the current date
if ((isNegativeOffset && remainingms > 0) || (isPositiveOffset && remainingms < 0)) {
return new Date(date.getTime() + ms);
}
// skip the weekend for a positive offset
date = isNegativeOffset ? weekBoundary : intervalDay.offset(weekBoundary, 2);
// add all of the complete weeks to the date
const completeWeeks = Math.floor(remainingms / millisPerWorkWeek);
date = intervalDay.offset(date, completeWeeks * 7);
remainingms -= completeWeeks * millisPerWorkWeek;
// add the remaining time
date = new Date(date.getTime() + remainingms);
return date;
};
skipWeekends.copy = function () { return skipWeekends; };
return skipWeekends;
};
export default () => base(date => date.getDay(), timeDay, timeSaturday, timeMonday);