function StackedBarChart(data, {
x = (d, i) => i,
y = d => d,
z = () => 1,
title,
marginTop = 30,
marginRight = 40,
marginBottom = 30,
marginLeft = 60,
width = 640,
height = 400,
xDomain,
xRange = [marginLeft, width - marginRight],
xPadding = 0.1,
yType = d3.scaleLinear,
yDomain,
yRange = [height - marginBottom, marginTop],
zDomain,
offset = d3.stackOffsetDiverging,
order = d3.stackOrderNone,
yFormat,
yLabel,
colors = d3.schemeTableau10,
} = {}) {
const X = d3.map(data, x);
const Y = d3.map(data, y);
const Z = d3.map(data, z);
if (xDomain === undefined) xDomain = X;
if (zDomain === undefined) zDomain = Z;
xDomain = new d3.InternSet(xDomain);
zDomain = new d3.InternSet(zDomain);
// Omit any data not present in the x- and z-domains.
const I = d3.range(X.length).filter(i => xDomain.has(X[i]) && zDomain.has(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, axes, and formats.
const xScale = d3.scaleBand(xDomain, xRange).paddingInner(xPadding);
const yScale = yType(yDomain, yRange);
const color = d3.scaleOrdinal(zDomain, colors);
const xAxis = d3.axisBottom(xScale).tickSizeOuter(0);
const yAxis = d3.axisLeft(yScale).ticks(height / 60, yFormat);
// Compute titles.
if (title === undefined) {
const formatValue = yScale.tickFormat(100, yFormat);
title = i => `${X[i]}\n${Z[i]}\n${formatValue(Y[i])}`;
} else {
const O = d3.map(data, d => d);
const T = title;
title = i => T(O[i], i, data);
}
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)
.attr("font-size", "12px")
.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("text-anchor", "middle")
.attr("transform", "translate(" + (-marginLeft + 20) + "," + height / 2 + ")rotate(-90)")
.attr("fill", "currentColor")
.attr("font-size", "16px")
.text(yLabel));
const formatValue = yScale.tickFormat(100, yFormat);
const bar = svg.append("g")
.selectAll("g")
.data(series)
.join("g")
.attr("fill", ([{i}]) => color(Z[i]))
.selectAll("rect")
.data(d => d)
.join("rect")
.attr("x", ({i}) => xScale(X[i]))
.attr("y", ([y1, y2]) => Math.min(yScale(y1), yScale(y2)))
.attr("height", ([y1, y2]) => Math.abs(yScale(y1) - yScale(y2)))
.attr("width", xScale.bandwidth())
.attr("id", ({i}) => `rect${i}`)
.on("mouseover", function(d, {i}) {
console.log("#rect" + i);
d3.select(this).attr('stroke', 'black');
d3.select("#rect" + i).attr('stroke', 'black').attr('stroke-width', '2px');
let splited_text = Z[i].split(' ');
tooltip
.html(
`<div>
<p>${X[i]}</p>
<p>${Z[i]}</p>
<p>+${formatValue(Y[i])}M</p>
</div>`)
.style('visibility', 'visible');
})
.on('mousemove', function (e) {
tooltip
.style('top', e.pageY - 40 + 'px')
.style('left', e.pageX + 10 + 'px')
.style("font-family", "Helvetica Neue, Arial");
})
.on('mouseout', function (d, {i}) {
tooltip.html(``).style('visibility', 'hidden');
d3.select(this).attr('stroke', 'none');
});
svg.append("g")
.attr("transform", `translate(0,${yScale(0)})`)
.call(xAxis)
.attr("font-size", "12px");
return Object.assign(svg.node(), {scales: {color}});
}