function Timeline(scaleX, margin, height = 240) {
let axis = {};
let nodes = {};
let originalScaleX = scaleX.copy();
const parts = ["yearly", "daily", "weekly", "grid", "yearlyGrid"];
const findDensityConfig = (map, value) => {
for (const [limit, config] of map) {
if (value < limit) {
return config;
}
}
return [];
};
const ensureTimeFormat = (value = "") => {
return typeof value !== "function" ? d3.utcFormat(value) : value;
};
axis["yearly"] = (parentNode, density) => {
const densityMap = [
[
3,
[
d3.utcMonth,
(d) => {
const startOfTheYear =
d.getUTCMonth() === 0 && d.getUTCDate() === 1;
const format = startOfTheYear ? "%Y – %B" : "%B";
return d3.utcFormat(format)(d);
},
],
],
[Infinity, [d3.utcYear, "%Y"]],
];
let [interval, format] = findDensityConfig(densityMap, density);
format = ensureTimeFormat(format);
const el = parentNode
.attr("transform", `translate(0,${margin.top - 48})`)
.call(
d3
.axisTop(scaleX)
.ticks(interval)
.tickFormat(format)
.tickSizeOuter(0)
);
el.select(".domain").remove();
el.selectAll("text")
.attr("y", 0)
.attr("x", 6)
.style("text-anchor", "start");
el.selectAll("line").attr("y1", -7).attr("y2", 6);
};
axis["daily"] = (parentNode, density) => {
const densityMap = [
[1, [d3.utcDay, "%-d"]],
[3, [d3.utcDay, ""]],
[8, [d3.utcMonth, "%B"]],
[13, [d3.utcMonth, "%b"]],
[22, [d3.utcMonth, (d) => d3.utcFormat("%B")(d).charAt(0)]],
[33, [d3.utcMonth.every(3), "Q%q"]],
[Infinity, [d3.utcMonth.every(3), ""]],
];
let [interval, format] = findDensityConfig(densityMap, density);
format = ensureTimeFormat(format);
const el = parentNode
.attr("transform", `translate(0,${margin.top - 28})`)
.call(
d3
.axisTop(scaleX)
.ticks(interval)
.tickFormat(format)
.tickSizeOuter(0)
);
el.select(".domain").remove();
el.selectAll("text")
.attr("y", 0)
.attr("x", 6)
.style("text-anchor", "start");
el.selectAll("line").attr("y1", -7).attr("y2", 0);
};
axis["weekly"] = (parentNode, density) => {
const densityMap = [
[10, [d3.utcMonday, (d) => +d3.utcFormat("%-V")(d)]], // monday as first of week and zero based
[33, [d3.utcMonday, ""]],
[Infinity, [d3.utcMonday.every(4), ""]],
];
let [interval, format] = findDensityConfig(densityMap, density);
format = ensureTimeFormat(format);
const el = parentNode
.attr("transform", `translate(0,${margin.top - 8})`)
.call(
d3
.axisTop(scaleX)
.ticks(interval)
.tickFormat(format)
.tickSizeOuter(0)
);
el.select(".domain").remove();
el.selectAll("line").style(
"visibility",
density > densityMap[0][0] ? "visible" : "hidden"
);
el.selectAll("text")
.attr("y", 0)
.attr("x", 6)
.style("text-anchor", "start");
el.selectAll("line").attr("y1", -7).attr("y2", 0);
};
axis["grid"] = (parentNode, density) => {
const densityMap = [
[1, [d3.utcDay]],
[8, [d3.utcMonday]],
[22, [d3.utcMonth]],
[Infinity, [d3.utcMonth.every(3)]],
];
const [interval] = findDensityConfig(densityMap, density);
const el = parentNode
.attr("transform", `translate(0,${margin.top})`)
.call(d3.axisTop(scaleX).ticks(interval).tickSizeOuter(0));
el.select(".domain").remove();
el.selectAll("text").remove();
el.selectAll("line")
.attr("y1", 0)
.attr("y2", height - margin.top - margin.bottom);
};
axis["yearlyGrid"] = (parentNode, density) => {
const densityMap = [
[3, [d3.utcMonth, "%B"]],
[Infinity, [d3.utcYear, "%Y"]],
];
let [interval, format] = findDensityConfig(densityMap, density);
format = ensureTimeFormat(format);
const el = parentNode
.attr("transform", `translate(0,${margin.top})`)
.call(
d3
.axisTop(scaleX)
.ticks(interval)
.tickFormat(format)
.tickSizeOuter(0)
);
el.select(".domain").remove();
el.selectAll("text").remove();
el.selectAll("line")
.attr("y1", 0)
.attr("y2", height - margin.top - margin.bottom);
};
const setup = () => {
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.classed("timeline", true)
.attr("style", "max-width: 100%; height: auto; height: intrinsic;");
const element = svg.node();
const rootNode = svg.append("g").classed("timeline-axis", true);
parts.forEach((part) => {
nodes[part] = rootNode.append("g").classed(part, true);
});
const update = () => {
// NOTE: Not used atm.
// const [startDate, endDate] = scaleX.domain();
// const totalVisibleDays = Math.abs(startDate - endDate) / MS_PER_DAY;
const density =
Math.abs(scaleX.invert(0) - scaleX.invert(1)) / MS_PER_HOUR; // in pixels per hour
parts.forEach((part) => {
nodes[part].call(axis[part], density);
});
};
const { zoom, reset } = zoomPlugin(svg,
({ transform }) => {
scaleX = transform.rescaleX(originalScaleX);
update();
}
);
update();
return {
element,
update,
reset,
};
};
return setup();
}