function StackedAreaChart(data, {
x = ([x]) => x,
y = ([, y]) => y,
z = () => 1,
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],
zDomain,
offset = d3.stackOffsetDiverging,
order = d3.stackOrderNone,
xFormat,
yFormat,
yLabel,
colors = d3.schemeTableau10,
yRightFormat = ".0%",
yRightLabel,
} = {}) {
const X = d3.map(data, x);
const Y = d3.map(data, y);
const Z = d3.map(data, z);
if (xDomain === undefined) xDomain = d3.extent(X);
if (zDomain === undefined) zDomain = Z;
zDomain = new d3.InternSet(zDomain); // create a set that supports Date object as a key
// Omit any data not present in the z-domain.
const I = d3.range(X.length).filter(i => zDomain.has(Z[i]));
console.log({X, Y, Z, I});
// Compute a nested array of series where each series is [[y1, y2], [y1, y2],
// [y1, y2], …] representing the y-extent of each stacked rect. In addition,
// each tuple has an i (index) property so that we can refer back to the
// original data point (data[i]). This code assumes that there is only one
// data point for a given unique x- and z-value.
const series = d3.stack()
.keys(zDomain)
.value(([x, I], z) => Y[I.get(z)])
.order(order)
.offset(offset)
(d3.rollup(I, ([i]) => i, i => X[i], i => Z[i]))
.map(s => s.map(d => Object.assign(d, {i: d.data[1].get(s.key)})));
// Compute the default y-domain. Note: diverging stacks can be negative.
if (yDomain === undefined) yDomain = d3.extent(series.flat(2));
// Construct scales and axes.
const xScale = xType(xDomain, xRange);
const yScale = yType(yDomain, yRange);
const color = d3.scaleOrdinal(zDomain, colors);
// const xAxis = d3.axisBottom(xScale).ticks(width / 80, xFormat).tickSizeOuter(0);
const xAxis = d3.axisBottom(xScale).ticks(d3.timeYear.every(1));
const yAxis = d3.axisLeft(yScale).ticks(height / 50, yFormat);
const area = d3.area()
.x(({i}) => xScale(X[i]))
.y0(([y1]) => yScale(y1))
.y1(([, y2]) => yScale(y2));
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(${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));
svg.append("g")
.selectAll("path")
.data(series)
.join("path")
.attr("fill", ([{i}]) => color(Z[i]))
.attr("d", area)
.append("title")
.text(([{i}]) => Z[i]);
svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(xAxis);
const L = I.map(i => (Y[i]+Y[i+1])*0.5) // helper array, only 0,2,4,6,8,10 indices are meaningful here
const line = d3.line()
.curve(d3.curveLinear)
.x(i => xScale(X[i]))
.y(i => yScale(L[i]));
svg.append("path")
.attr("fill", "none")
.attr("stroke", "#ce5e5b")
.attr("stroke-width", 1.5)
.attr("stroke-linecap", "round")
.attr("stroke-linejoin", "round")
.attr("stroke-opacity", 1)
.attr("d", line([0,2,4,6,8,10]));
return Object.assign(svg.node(), {scales: {color}});
}