chart = {
const height = 500;
const margin = {top: 40, right: 30, bottom: 30, left: 50};
const xOccupancy = d3.scaleLinear()
.domain([(startingOccupancy / 100), 0])
.range([margin.left, width - margin.right]);
const xMax = xOccupancy(steps.at(-1).occupancyPct);
const xStep = d3.scaleLinear()
.domain(d3.extent(steps, (d) => d.step))
.range([margin.left, xMax]);
const y = d3.scaleLinear()
.domain([0, d3.max(steps, d => d.dose) + 5])
.range([height - margin.bottom, margin.top]);
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.attr("width", width)
.attr("height", height)
.attr("style", "max-width: 100%; height: auto; height: intrinsic; font: 10px sans-serif;")
.style("-webkit-tap-highlight-color", "transparent")
.style("overflow", "visible");
svg.append("g")
.attr("transform", `translate(0,${margin.top})`)
.call(d3.axisTop(xOccupancy).tickFormat(formatPct))
.call(g =>
g.append("text")
.attr("x", width - margin.right)
.attr("y", 16)
.attr("text-anchor", "end")
.attr("fill", "currentColor")
.text("Target serotonin receptor occupancy")
);
svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(xStep).ticks(steps.length).tickFormat(d => d * form.stepInterval))
.call(g =>
g.select(".domain")
.attr("d", function() {
return d3.select(this).attr("d").replace(xMax, width - margin.right).replace(/V\d+$/, "");
})
)
.call(g =>
g.append("text")
.attr("x", width - margin.right)
.attr("y", -10)
.attr("text-anchor", "end")
.attr("fill", "currentColor")
.text("Day")
);
svg.append("g")
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y).tickFormat(formatDose))
.call(g =>
g.append("text")
.attr("x", -margin.left)
.attr("y", 10)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text("Dose (mg)")
);
const lineFactory = () => d3.line()
.x(d => xOccupancy(d.occupancyPct))
.y(d => y(d.dose));
const stepPath = svg.append("path")
.datum(steps)
.attr("fill", "none")
.attr("stroke", "#eee")
.attr("stroke-width", 1.5)
.attr("d", lineFactory().curve(d3.curveStepBefore))
.lower();
const path = svg.append("path")
.datum(steps)
.attr("fill", "none")
.attr("stroke", "MediumPurple")
.attr("stroke-width", 1.5)
.attr("d", lineFactory())
const circles = svg.append("g")
.attr("fill", "MediumPurple")
.selectAll("circle")
.data(steps)
.join("circle")
.attr("cx", d => xOccupancy(d.occupancyPct))
.attr("cy", d => y(d.dose))
.attr("r", 3);
const pathOverlay = svg.append("path")
.datum(steps)
.attr("fill", "none")
.attr("stroke", "transparent")
.attr("stroke-width", 20)
.attr("d", lineFactory())
.on("pointerenter pointermove", pointermoved)
.on("pointerleave", pointerleft)
.on("touchstart", event => event.preventDefault());
const tooltip = svg.append("g")
.style("pointer-events", "none");
// Add the event listeners that show or hide the tooltip.
const bisect = d3.bisector((d, x) => x - d.occupancyPct).center;
function pointermoved(event) {
const i = bisect(steps, xOccupancy.invert(d3.pointer(event)[0]));
tooltip.style("display", null);
const yPos = y(steps[i].dose);
tooltip.attr("transform", `translate(${xOccupancy(steps[i].occupancyPct)},${yPos})`);
const path = tooltip.selectAll("path")
.data([,])
.join("path")
.attr("fill", "white")
.attr("stroke", "black")
.attr("transform", "");
const text = tooltip.selectAll("text")
.data([,])
.join("text")
.call(text => text
.selectAll("tspan")
.data([
`Step ${steps[i].step}`,
formatDose(formatDoseNumber(steps[i].dose)),
formatPct(steps[i].occupancyPct) + ' occupancy',
])
.join("tspan")
.attr("x", 0)
.attr("y", (_, i) => `${i * 1.1}em`)
.attr("font-weight", (_, i) => i ? null : "bold")
.text(d => d)
);
size(text, path);
flip(text, path, yPos);
}
function pointerleft() {
tooltip.style("display", "none");
}
// Wraps the text with a callout path of the correct size, as measured in the page.
function size(text, path) {
const {x, y, width: w, height: h} = text.node().getBBox();
text.attr("transform", `translate(${-w / 2},${15 - y})`);
path.attr("d", `M${-w / 2 - 10},5H-5l5,-5l5,5H${w / 2 + 10}v${h + 20}h-${w + 20}z`);
}
function flip(text, path, yPos) {
const {width: w, height: h} = path.node().getBBox();
if (height - yPos - h <= 0) {
path.attr("transform", "rotate(180)");
const transform = text.attr("transform");
const {y: yText} = text.node().getBBox();
text.attr("transform", transform.replace(/translate\(([\d.-]+),\s?[\d.-]+\)/, `translate($1,${10 - h - yText})`));
}
}
return svg.node();
}