function stackedToGroupedBarChart() {
d3.select("#chart").selectAll("svg").remove();
let width,
height,
groups,
legendLabels,
xAxisLabels,
yAxisLabels,
formatBarLabel,
initialLayout = "stacked";
let svg,
rect,
rectLabel,
layout,
groupedLabels,
stackedLabels,
o,
n,
m,
xz,
yz,
y01z,
yMax,
y1Max,
y2Max,
x,
y,
z,
margin = { top: 50, right: 0, bottom: 22, left: 40 };
// generate chart
function chart(selection) {
selection.each(function (data) {
// initialize svg
svg = selection.append("svg").attr("width", width).attr("height", height);
// initialize layout
layout = initialLayout;
// calculate number of series
n = Object.keys(groups[0].stacked).length;
// o = Object.keys(groups[0].grouped).length;
// calculate number of vals per series
m = groups.length;
// calculate x-values shared by all series
xz = d3.range(m);
// calculate y-values of each of the n series
yz = {
stacked: d3.range(n).map((i) => {
return d3.range(m).map((gi) => groups[gi].stacked[i] / 1);
}),
grouped: d3.range(n).map((i) => {
return d3.range(m).map((gi) => groups[gi].grouped[i] / 1);
})
};
// calculate stacked y-values of each of the n series
y01z = {
stacked: d3
.stack()
.keys(d3.range(n))(d3.transpose(yz.stacked))
.map((data, i) => data.map(([y0, y1]) => [y0, y1, i])),
grouped: d3
.stack()
.keys(d3.range(n))(d3.transpose(yz.grouped))
.map((data, i) => data.map(([y0, y1]) => [y0, y1, i]))
};
// configure y maxes
yMax = d3.max(yz[layout], (y) => d3.max(y));
y1Max = d3.max(y01z[layout], (y) => d3.max(y, (d) => d[1]));
// y2Max = d3.max(y01z[layout], (y) => d3.max(y, (d) => d[0]));
// configure x scale
x = d3
.scaleBand()
.domain(xz)
.rangeRound([margin.left, width - margin.right])
.padding(0.18);
// configure y scale
y = d3
.scaleLinear()
.domain([0, y1Max])
.range([height - margin.bottom, margin.top]);
// yAx = d3
// .scaleLinear()
// .domain([0, y1Max])
// .range([height - margin.bottom, margin.top]);
// configure z scale (color dimension)
z = d3.scaleSequential(d3.schemePastel1).domain([-0.5 * n, 0.65 * n]);
// initialize bars
rect = svg
.selectAll("g")
.data(y01z[layout])
.enter()
.append("g")
.attr("class", "bar-group")
.attr("fill", (d, i) => z(i))
.selectAll("rect")
.data((d) => d)
.join("rect")
.attr("class", "bar")
.attr("x", (d, i) => x(i))
.attr("y", height - margin.bottom)
.attr("width", x.bandwidth())
.attr("height", 0);
// let buffer = margin.top - 2;
// initialize bar labels
rectLabel = svg
.append("g")
.attr("class", "bar-labels")
.selectAll("g")
.data(y01z[layout])
.enter()
.append("g")
.attr("class", "bar-label")
.selectAll("rect")
.data((d) => d)
.join("text")
.attr("x", (d, i) => x(i))
.attr("y", height - margin.top)
.attr("font-size", "0.75rem")
//.attr("font-weight", 100)
.attr("font-family", "System monospace")
.attr("stroke", "rgba(20,5,15,0.05)")
.attr("opacity", 0.34)
.text((d, i) => {
let val = groups[i][layout][d[2]];
return "function" === typeof formatBarLabel
? formatBarLabel(val)
: val;
});
// render x-axis
svg.call(xAxis);
// render y-axis
svg.call(yAxis);
// render legend
svg.call(legend);
if (layout === "stacked") {
transitionStacked();
svg.call(yAxis);
}
// if (layout === "grouped") transitionGrouped();
if (layout === "grouped") {
transitionGrouped();
svg.call(yAxis);
}
});
}
// --- helpers ------------------------------------------------
function transitionGrouped(params) {
// configure timing vars
let tDuration = 500,
tDelay = 20;
// update y scale domain
y.domain([0, y1Max]);
// animate bars
rect
.transition()
.duration(tDuration)
.delay((d, i) => i * tDelay)
.attr("x", (d, i) => x(i) + (x.bandwidth() / n) * d[2])
.attr("width", x.bandwidth() / n)
.transition()
.attr("y", (d) => y(d[1] - d[0]))
.attr("height", (d) => y(0) - y(d[1] - d[0]));
// animate bar labels
rectLabel.each(function (d, i) {
let width = d3.select(this).node().getBBox().width;
d3.select(this)
.transition()
.duration(tDuration)
.delay(i * tDelay)
.attr(
"x",
x(i) +
(x.bandwidth() / n) * d[2] +
x.bandwidth() / n / 2 -
width / 2 +
4
)
// .attr("y", y(d))
.on("end", () =>
params && typeof params.atHalfway === "function"
? params.atHalfway()
: null
)
.transition()
.attr("y", y(d[1] - d[0]) - 2)
.attr("opacity", 1);
});
}
function transitionStacked(params) {
// configure timing vars
let tDuration = 500,
tDelay = 20;
// update y scale domain
y.domain([0, y1Max]);
// animate bars
rect
.transition()
.duration(tDuration)
.delay((d, i) => i * tDelay)
.attr("y", (d) => y(d[1]))
.attr("height", (d) => y(d[0]) - y(d[1]))
.transition()
.attr("x", (d, i) => x(i))
.attr("width", x.bandwidth());
// animate bar labels
rectLabel.each(function (d, i) {
let width = d3.select(this).node().getBBox().width;
d3.select(this)
.transition()
.duration(tDuration)
.delay(i * tDelay)
.attr("y", (d) => y(d[1]) + (y(d[0]) - y(d[1])) / 1.35)
.on("end", () =>
params && typeof params.atHalfway === "function"
? params.atHalfway()
: null
)
.transition()
.attr("x", x(i) + (x.bandwidth() - width) / 2)
.attr("opacity", 1);
});
}
function xAxis(selection) {
selection.each(function () {
let _this = d3.select(this);
// remove previous x-axis
_this.selectAll("g.x-axis").remove();
// add new x-axis
svg
.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0, ${height - margin.bottom})`)
.call(
d3
.axisBottom(x)
.tickSizeOuter(0)
.tickFormat((i) => xAxisLabels[layout][i])
);
});
}
function yAxis(selection) {
selection.each(function () {
let _this = d3.select(this);
// remove previous y-axis
_this.selectAll("g.y-axis").remove();
// add new y-axis
svg
.append("g")
.attr("class", "y-axis")
.attr("transform", `translate(${margin.left - 10}, 0)`)
.call(
d3.axisLeft(y).tickSizeOuter(0)
//.tickFormat((i) => yAxisLabels[layout][i])
);
});
}
function legend(selection) {
selection.each(function () {
let _this = d3.select(this);
let shapePadding = 50,
shapeSize = 220,
offsetLeft = 8;
let ordinal = d3
.scaleOrdinal()
.domain(legendLabels)
.range(legendLabels.map((l, i) => z(i)));
let legend = d3
.legendColor()
.orient("horizontal")
// .orient("vertical")
.shapeWidth(width / legendLabels.length)
.labelOffset(8)
.labelWrap(width / legendLabels.length)
.scale(ordinal);
// remove previous legend
_this.selectAll("g.legend").remove();
// add new legend
_this
.append("g")
.attr("class", "legend")
.attr("transform", `translate(30, 0)`)
.call(legend);
});
}
// --- updaters -------------------------------------------------
chart.update = function (value) {
if (!arguments.length || !value || layout === value) return;
// update global layout var
layout = value;
// update y maxes
yMax = d3.max(yz[layout], (y) => d3.max(y));
y1Max = d3.max(y01z[layout], (y) => d3.max(y, (d) => d[1]));
// rebind bar rect data to values from new layout
svg
.selectAll("g.bar-group")
.data(y01z[layout])
.selectAll("rect")
.data((d) => d);
// rebind bar label data to values from new layout
svg
.selectAll("g.bar-label")
.data(y01z[layout])
.selectAll("text")
.data((d) => d);
const updateBarLabelText = () =>
svg
.selectAll("g.bar-label")
.selectAll("text")
.text((d, i) => {
let val = groups[i][layout][d[2]];
return "function" === typeof formatBarLabel
? formatBarLabel(val)
: val;
});
//(val) => (layout == "grouped" ? val + "pts" : val + "%")
// rerender x-axis to reflect labels from new layout
//svg.call(xAxis);
svg.call(yAxis);
// transition to new layout
if (value === "stacked")
transitionStacked({ atHalfway: updateBarLabelText });
if (value === "grouped")
transitionGrouped({ atHalfway: updateBarLabelText });
};
// --- accessors ------------------------------------------------
chart.width = function (value) {
if (!arguments.length) return width;
width = value;
return chart;
};
chart.height = function (value) {
if (!arguments.length) return height;
height = value;
return chart;
};
chart.initialLayout = function (value) {
if (!arguments.length) return initialLayout;
initialLayout = value;
return chart;
};
chart.groups = function (value) {
if (!arguments.length) return groups;
groups = value;
return chart;
};
chart.legendLabels = function (value) {
if (!arguments.length) return legendLabels;
legendLabels = value;
return chart;
};
chart.xAxisLabels = function (value) {
if (!arguments.length) return xAxisLabels;
xAxisLabels = value;
return chart;
};
chart.yAxisLabels = function (value) {
if (!arguments.length) return yAxisLabels;
yAxisLabels = value;
// yAxisLabels.formatBarLabel((val) =>
// layout == "grouped" ? val + "pts" : val + "%"
// );
return chart;
};
chart.formatBarLabel = function (value) {
if (!arguments.length) return formatBarLabel;
formatBarLabel = (value) =>
layout === "grouped" ? value + "" : value + "%";
return chart;
};
return chart;
}