function LineChart(
data,
{
x = ([x]) => x,
y = ([, y]) => y,
z = () => 1,
title,
defined,
curve = d3.curveLinear,
marginTop = 20,
marginRight = 30,
marginBottom = 30,
marginLeft = 40,
width = 640,
height = 400,
xType = d3.scaleUtc,
xDomain,
xRange = [marginLeft, width - marginRight],
yType = d3.scaleLinear,
yDomain,
yRange = [height - marginBottom, marginTop],
yFormat,
yLabel,
zDomain,
strokeLinecap,
strokeLinejoin,
strokeWidth = 3,
strokeOpacity,
mixBlendMode = "multiply",
voronoi,
colors = d3.schemeTableau10
} = {}
) {
// Compute values.
const X = d3.map(data, x);
const Y = d3.map(data, y);
const Z = d3.map(data, z);
const O = d3.map(data, (d) => d);
if (defined === undefined) defined = (d, i) => !isNaN(X[i]) && !isNaN(Y[i]);
const D = d3.map(data, defined);
// Compute default domains, and unique the z-domain.
if (xDomain === undefined) xDomain = d3.extent(X);
if (yDomain === undefined)
yDomain = [0, d3.max(Y, (d) => (typeof d === "string" ? +d : d))];
if (zDomain === undefined) zDomain = Z;
zDomain = new d3.InternSet(zDomain);
// Omit any data not present in the z-domain.
const I = d3.range(X.length).filter((i) => zDomain.has(Z[i]));
// Construct scales and axes.
const xScale = xType(xDomain, xRange);
const yScale = yType(yDomain, yRange);
const xAxis = d3
.axisBottom(xScale)
.ticks(width / 80)
.tickSizeOuter(0);
const yAxis = d3.axisLeft(yScale).ticks(height / 60, yFormat);
const color = d3.scaleOrdinal(zDomain, colors);
// Compute titles.
if (title === undefined) {
const formatDate = xScale.tickFormat(null, "%b %-d, %Y");
const formatValue = yScale.tickFormat(100, yFormat);
title = (i) => `${formatDate(X[i])}\n${formatValue(Y[i])}`;
} else {
const O = d3.map(data, (d) => d);
const T = title;
title = (i) => T(O[i], i, data);
}
// Construct a line generator.
const line = d3
.line()
.defined((i) => D[i])
.curve(curve)
.x((i) => xScale(X[i]))
.y((i) => yScale(Y[i]));
const svg = d3
.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.style("-webkit-tap-highlight-color", "transparent")
.style("overflow", "visible")
.on("pointerenter pointermove", pointermoved)
.on("pointerleave", pointerleft)
.on("touchstart", (event) => event.preventDefault());
// An optional Voronoi display (for fun).
if (voronoi)
svg
.append("path")
.attr("fill", "none")
.attr("stroke", "#ccc")
.attr(
"d",
d3.Delaunay.from(
I,
(i) => xScale(X[i]),
(i) => yScale(Y[i])
)
.voronoi([0, 0, width, height])
.render()
);
svg
.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(xAxis);
svg
.append("g")
.attr("transform", `translate(${marginLeft},0)`)
.call(yAxis)
.call((g) => g.select(".domain").remove())
.call(
voronoi
? () => {}
: (g) =>
g
.selectAll(".tick line")
.clone()
.attr("x2", width - marginLeft - marginRight)
.attr("stroke-opacity", 0.1)
)
.call((g) =>
g
.append("text")
.attr("x", -marginLeft)
.attr("y", 10)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text(yLabel)
);
const path = svg
.append("g")
.attr("fill", "none")
.attr("stroke", typeof color === "string" ? color : null)
.attr("stroke-linecap", strokeLinecap)
.attr("stroke-linejoin", strokeLinejoin)
.attr("stroke-width", strokeWidth)
.attr("stroke-opacity", strokeOpacity)
.selectAll("path")
.data(d3.group(I, (i) => Z[i]))
.join("path")
.style("mix-blend-mode", mixBlendMode)
.attr("stroke", typeof color === "function" ? ([z]) => color(z) : null)
.attr("d", ([, I]) => line(I));
const dot = svg.append("g").attr("display", "none");
dot.append("circle").attr("r", 2.5);
dot
.append("text")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.attr("text-anchor", "middle")
.attr("y", -8);
const tooltip = svg.append("g").style("pointer-events", "none");
function pointermoved(event) {
const i = d3.bisectCenter(X, xScale.invert(d3.pointer(event)[0]));
tooltip.style("display", null);
tooltip.attr("transform", `translate(${xScale(X[i])},${yScale(Y[i])})`);
const path = tooltip
.selectAll("path")
.data([,])
.join("path")
.attr("fill", "white")
.attr("stroke", "black");
const text = tooltip
.selectAll("text")
.data([,])
.join("text")
.call((text) =>
text
.selectAll("tspan")
.data(`${title(i)}`.split(/\n/))
.join("tspan")
.attr("x", 0)
.attr("y", (_, i) => `${i * 1.1}em`)
.attr("font-weight", (_, i) => (i ? null : "bold"))
.text((d) => d)
);
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`
);
svg.property("value", O[i]).dispatch("input", { bubbles: true });
}
function pointerleft() {
tooltip.style("display", "none");
svg.node().value = null;
svg.dispatch("input", { bubbles: true });
}
return Object.assign(svg.node(), { value: null });
}