viewof timescale = {
const svg = d3
.create("svg")
.attr("viewBox", [0, 0, width, height + margins.bottom])
.style("font", font);
const g = svg.append("g");
const element = svg.node();
let focus = root;
element.value = { selected: names[0], sequence: [] };
element.dispatchEvent(new CustomEvent("input"));
let hideSmallTicks = true;
const cellGroup = g.append("g").attr("id", "cells");
const cell = cellGroup
.selectAll("g")
.data(root.descendants())
.join("g")
.attr("transform", (d) => `translate(${d.x0},${d.y0})`);
const rect = cell
.append("rect")
.attr("width", (d) => d.x1 - d.x0)
.attr("height", (d) => d.y1 - d.y0)
.attr("fill", (d) => d.data.color)
.attr("stroke", "white")
.attr("stroke-width", 0.5)
.attr("cursor", "pointer")
.on("pointerenter", (event, d) => {
const sequence = d.ancestors().reverse();
cell.attr("fill-opacity", (d) => {
return sequence.includes(d) ? 1.0 : 0.5;
});
// Update the value of this view with the currently hovered sequence
element.value = { ...element.value, sequence };
element.dispatchEvent(new CustomEvent("input"));
})
.on("click", clicked);
svg.on("pointerleave", () => {
cell.attr("fill-opacity", 1);
// Update the value of this view
element.value = { ...element.value, sequence: [] };
element.dispatchEvent(new CustomEvent("input"));
});
cell.append("title").text((d) => {
const sequence = d
.ancestors()
.map((d) => d.data.name)
.reverse();
return `${sequence.join(" > ")}`;
});
const text = cell
.append("text")
.style("user-select", "none")
.attr("pointer-events", "none")
.attr("x", (d) => {
const textX = (d.x1 - d.x0) / 2;
return Number.isNaN(textX) ? -1000 : textX; // not sure checking NaN is necessary
})
.attr("y", (d) => (d.y1 - d.y0) / 2)
.attr("fill", (d) => d.data.textColor ?? "black")
.attr("fill-opacity", labelVisible)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.text((d) => {
const rectWidth = d.x1 - d.x0;
const labelWidth = getTextWidth(d.data.name, font);
const abbrev = d.data.abr || d.data.name.charAt(0);
return rectWidth - 10 < labelWidth ? abbrev : d.data.name;
});
// Append ages ticks scale bar
const ticksGroup = g
.append("g")
.attr("id", "ticks")
.attr("transform", `translate(0,${height - margins.bottom})`); // Move tick group down
ticksGroup.call((g) => ticks(g, makeTicksData(root), hideSmallTicks));
svg.call(
d3
.zoom()
.extent([
[0, 0],
[width, height]
])
.scaleExtent([1, 8])
.on("zoom", zoomed)
.on("end", () => {
rect.attr("cursor", "pointer");
})
);
if (selectedInterval) {
const matchNode = root.find((d) => d.data.name === selectedInterval);
clicked(null, matchNode);
}
function clicked(event, p) {
focus = p === focus ? p.parent : p;
hideSmallTicks = [0, 1].includes(focus.depth);
// sync input to clicked node
if (event) {
set(viewof selectInterval[0], [focus.data.name]);
}
const focusAncestors = focus.ancestors().slice(1); // Ignore clicked node itself
element.value = { ...element.value, selected: focus.data.name };
element.dispatchEvent(new CustomEvent("input"));
const t = event
? d3.transition().duration(450).ease(d3.easeCubicInOut)
: null; // Can't transition when using input, bit of a hack
// Show a bit of the neighbouring cells on focus of an interval
const leftNeighbor =
focus.data.start === root.data.start ? 0 : neighborWidth;
const rightNeighbor = focus.data.end === root.data.end ? 0 : neighborWidth;
const focusWidth = focus.x1 - focus.x0; // partition width of focused node
root.each((d) => {
const widthMinusNeighbors = width - rightNeighbor - leftNeighbor;
const target = {
x0:
leftNeighbor + ((d.x0 - focus.x0) / focusWidth) * widthMinusNeighbors,
x1:
leftNeighbor + ((d.x1 - focus.x0) / focusWidth) * widthMinusNeighbors,
y0: d.y0,
y1: d.y1
};
d.target = target;
});
// Reset drag
g.transition(t).attr("transform", "translate(0,0)");
cell
.transition(t)
.attr("transform", (d) => `translate(${d.target.x0},${d.target.y0})`);
rect
.transition(t)
.attr("width", (d) => d.target.x1 - d.target.x0)
.attr("stroke", "white")
.attr("stroke-width", 1);
if (event) {
d3.select(this)
.transition(t)
.attr("stroke", "black")
.attr("stroke-width", 1.5);
d3.select(this.parentNode).raise();
}
text
.transition(t)
.attr("fill-opacity", (d) =>
focusAncestors.includes(d) ? 1 : labelVisible(d.target)
)
.attr("x", (d) => {
// Position all the ancestors labels in the middle
if (focusAncestors.includes(d)) {
return -d.target.x0 + width / 2;
}
const rectWidth = d.target.x1 - d.target.x0;
const textX = rectWidth / 2;
return Number.isNaN(textX) ? width / 2 : textX;
})
.text((d) => {
const rectWidth = d.target.x1 - d.target.x0;
const labelWidth = getTextWidth(d.data.name, font);
const abbrev = d.data.abr || d.data.name.charAt(0);
return rectWidth - 8 < labelWidth ? abbrev : d.data.name;
});
ticksGroup.call((g) => ticks(g, makeTicksData(root), hideSmallTicks, t));
}
function zoomed(e) {
if (!root.target) return;
const translateX = e.transform.x;
// Do not allow scrolling beyond left- and rightmost node
// These conditions are not completely correct🚨
if (
translateX + root.target.x0 > 0 ||
root.x1 - translateX > root.target.x1
)
return;
rect.attr("cursor", "grabbing");
g.attr("transform", `translate(${translateX},0)`);
}
return element;
}