function BarChart(
data,
{
x = (d) => d,
y = (d, i) => i,
title,
marginTop = 30,
marginRight = 0,
marginBottom = 10,
marginLeft = 30,
width = 640,
height,
xType = d3.scaleLinear,
xDomain,
xRange = [marginLeft, width - marginRight],
xFormat,
xLabel,
yPadding = 0.1,
yDomain,
yRange,
color = "currentColor",
titleColor = "white",
titleAltColor = "currentColor",
duration: initialDuration = 500,
delay: initialDelay = (_, i) => i * 20
} = {}
) {
const X = d3.map(data, x);
const Y = d3.map(data, y);
if (xDomain === undefined) xDomain = [0, d3.max(X)];
if (yDomain === undefined) yDomain = Y;
yDomain = new d3.InternSet(yDomain);
// Omit any data not present in the y-domain.
const I = d3.range(X.length).filter((i) => yDomain.has(Y[i]));
// Compute the default height.
if (height === undefined)
height =
Math.ceil((yDomain.size + yPadding) * 25) + marginTop + marginBottom;
if (yRange === undefined) yRange = [marginTop, height - marginBottom];
// Construct scales and axes.
const xScale = xType(xDomain, xRange);
const yScale = d3.scaleBand(yDomain, yRange).padding(yPadding);
const xAxis = d3.axisTop(xScale).ticks(width / 80, xFormat);
const yAxis = d3.axisLeft(yScale).tickSizeOuter(0);
// Compute titles.
// if (title === undefined) {
// const formatValue = xScale.tickFormat(100, xFormat);
// title = (i) => `${formatValue(X[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;");
let xGroup = svg
.append("g")
.attr("transform", `translate(0,${marginTop})`)
.call(xAxis)
.call((g) => g.select(".domain").remove())
.call((g) =>
g
.selectAll(".tick line")
.clone()
.attr("y2", height - marginTop - marginBottom)
.attr("stroke-opacity", 0.1)
)
.call((g) =>
g
.append("text")
.attr("x", width - marginRight)
.attr("y", -22)
.attr("fill", "currentColor")
.attr("text-anchor", "end")
.text(xLabel)
);
let rect = svg
.append("g")
.selectAll("rect")
.data(I)
.join("rect")
.property("key", (i) => Y[i]) // for future transitions
.call(
position,
(i) => xScale(X[i]),
(i) => yScale(Y[i])
)
.attr("fill", (d, i) => color(data[i].gender));
// A helper method for updating the position of bars.
function position(rect, x, y) {
return rect
.attr("x", xScale(0))
.attr("y", y)
.attr("width", (i) => x(i) - xScale(0))
.attr("height", yScale.bandwidth());
}
// svg
// .append("g")
// .attr("fill", titleColor)
// .attr("text-anchor", "end")
// .attr("font-family", "sans-serif")
// .attr("font-size", 10)
// .selectAll("text")
// .data(I)
// .join("text")
// .attr("x", (i) => xScale(X[i]))
// .attr("y", (i) => yScale(Y[i]) + yScale.bandwidth() / 2)
// .attr("dy", "0.35em")
// .attr("dx", -4)
// .text(title)
// .call((text) =>
// text
// .filter((i) => xScale(X[i]) - xScale(0) < 20) // short bars
// .attr("dx", +4)
// .attr("fill", titleAltColor)
// .attr("text-anchor", "start")
// );
let yGroup = svg
.append("g")
.attr("transform", `translate(${marginLeft},0)`)
.call(yAxis);
// return svg.node();
// Call chart.update(data, options) to transition to new data.
return Object.assign(svg.node(), {
update(
data,
{
xDomain, // an array of (ordinal) x-values
yDomain, // [ymin, ymax]
duration = initialDuration, // transition duration, in milliseconds
delay = initialDelay // per-element transition delay, in milliseconds
} = {}
) {
// Compute values.
const X = d3.map(data, x);
const Y = d3.map(data, y);
// Compute default domains, and unique the y-domain.
if (xDomain === undefined) xDomain = [0, d3.max(X)];
if (yDomain === undefined) yDomain = Y;
yDomain = new d3.InternSet(yDomain);
// Omit any data not present in the x-domain.
const I = d3.range(X.length).filter((i) => yDomain.has(Y[i]));
// Compute the default height.
if (height === undefined)
height =
Math.ceil((yDomain.size + yPadding) * 25) + marginTop + marginBottom;
if (yRange === undefined) yRange = [marginTop, height - marginBottom];
// Update scale domains.
xScale.domain(xDomain);
yScale.domain(yDomain);
// Start a transition.
const t = svg.transition().duration(duration);
// Join the data, applying enter and exit.
rect = rect
.data(I, function (i) {
return this.tagName === "rect" ? this.key : Y[i];
})
.join(
(enter) =>
enter
.append("rect")
.property("key", (i) => Y[i]) // for future transitions
.call(
position,
(i) => xScale(X[i]),
(i) => yScale(Y[i])
)
.style("mix-blend-mode", "multiply")
.call((enter) => enter.append("title")),
(update) => update,
(exit) =>
exit
.transition(t)
.delay(delay)
.attr("y", yScale(0))
.attr("height", 0)
.remove()
);
// Update the title text on all entering and updating bars.
// rect.select("title").text((i) => [X[i], format(Y[i])].join("\n"));
// Transition entering and updating bars to their new position. Note
// that this assumes that the input data and the x-domain are in the
// same order, or else the ticks and bars may have different delays.
rect
.transition(t)
.delay(delay)
.call(
position,
(i) => xScale(X[i]),
(i) => yScale(Y[i])
);
// Transition the x-axis (using a possibly staggered delay per tick).
xGroup
.transition(t)
.call(xAxis)
.call((g) => g.selectAll(".tick").delay(delay));
// Transition the y-axis, then post process for grid lines etc.
yGroup
.transition(t)
.call(yAxis)
.selection()
.call((g) => g.select(".domain").remove());
// .call((g) =>
// g.selectAll(".tick").selectAll(".grid").data([,]).join(grid)
// );
}
});
}