function LineChart(data, {
x = ([x]) => x,
y = ([, y]) => y,
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],
xFormat,
yFormat,
yLabel,
color = "currentColor",
strokeLinecap = "round",
strokeLinejoin = "round",
strokeWidth = 1.5,
strokeOpacity = 1,
} = {}) {
const X = d3.map(data, x);
const Y = d3.map(data, y);
const I = d3.range(X.length);
if (defined === undefined) defined = (d, i) => !isNaN(X[i]) && !isNaN(Y[i]);
const D = d3.map(data, defined);
if (xDomain === undefined) xDomain = d3.extent(X);
if (yDomain === undefined) yDomain = [d3.min(Y) - 1, d3.max(Y) + 1];
// Construct scales and axes.
const xScale = xType(xDomain, xRange);
const yScale = yType(yDomain, yRange);
const xAxis = d3.axisBottom(xScale).ticks(width / 80, xFormat).tickSizeOuter(0);
const yAxis = d3.axisLeft(yScale).ticks(height / 40, yFormat);
// 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;");
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(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));
function tweenDash() {
const l = this.getTotalLength(),
i = d3.interpolateString("0," + l, l + "," + l);
return function (t) {
return i(t);
};
}
function transition(path) {
path
.transition()
.duration((X.length - 1) * 1000)
.ease(d3.easeLinear)
.attrTween("stroke-dasharray", tweenDash);
}
svg.append("path")
.attr("fill", "none")
.attr("stroke", color)
.attr("stroke-width", strokeWidth)
.attr("stroke-linecap", strokeLinecap)
.attr("stroke-linejoin", strokeLinejoin)
.attr("stroke-opacity", strokeOpacity)
.attr("d", line(I))
.call(transition);
const textPadding = 5;
svg
.append("g")
.attr("text-anchor", "middle")
.selectAll("text")
.data(I)
.enter()
.append("text")
.attr("x", (i) => xScale(X[i]))
.attr("y", (i) => {
let a = Y[i - 1];
let b = Y[i];
let c = Y[i + 1];
if (a <= b && b >= c) {
return yScale(b) - textPadding;
} else {
return yScale(b) + 15;
}
})
.attr('fill', '#ec4899')
.text((i) => Y[i]);
svg
.append("g")
.selectAll("circle")
.data(I)
.enter()
.append("circle")
.attr("r", 3)
.attr("cx", (i) => xScale(X[i]))
.attr("cy", (i) => yScale(Y[i]))
.attr("fill", "transparent")
.transition()
.ease(d3.easeLinear)
.delay((d) => d * 1000)
.attr("fill", color);
return svg.node();
}