Published
Edited
Aug 27, 2020
Importers
4 stars
Insert cell
Insert cell
chart = {
const svg = d3.create("svg").attr('width', width);

svg
.selectAll("rect")
.data(bars)
.join("rect")
.attr("fill", d => color(d._.name))
.attr("x", d => d.pos * size + margin.left)
.attr("y", 0)
.attr("width", d => d.size * size)
.attr("height", barHeight)
.append("title")
.text(d => `${d._.name}: ${d._.value.toLocaleString()}`);

let has = { above: false, below: false };

svg
.append("g")
.attr(
"font-family",
"-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji"
)
.attr("font-size", 12)
.attr("text-anchor", "middle")
.selectAll("text")
.data(bars.filter(d => !d._.hideLabel))
.join("text")
.call(text =>
text
.append("tspan")
.attr("y", "-0.4em")
.attr("font-weight", "bold")
.attr("class", "name-span")
.text(d => d._.name)
)
.call(text =>
text
.append("tspan")
.attr("x", 0)
.attr("y", "0.7em")
.attr("fill-opacity", 0.7)
.attr("class", "value-span")
.text(d => d._.value.toLocaleString())
)
.each(function(d, i) {
const nameSpan = this.querySelector('.name-span');
const valueSpan = this.querySelector('.value-span');

const nameLength = getLength(nameSpan);
const valueLength = getLength(valueSpan);
const textWidth = Math.max(nameLength, valueLength);

const x = (d.pos + d.size / 2) * size + margin.left;
let y = barHeight / 2;

const barIsDark = isDark(color(d._.name));

// move the text spans outside of the slice and add an indicator pointing to the slice
if (textWidth > d.size * size) {
const orientation = i % 2 === 0 ? 'below' : 'above';

insertIndicator(this, x, {
orientation,
barIsDark,
showInnerLine: d.size * size > 2
});
if (orientation === 'below') {
has.below = true;
y = barHeight + 30;
} else {
has.above = true;
y = -25;
}
} else {
// only apply the white fill if the bar is dark and the label is overlaying the bar,
// since the background of the SVG is always white and black text will look great
// when the label is offset.
if (barIsDark) {
this.setAttribute('fill', 'white');
}
this.setAttribute('y', barHeight);
}
this.setAttribute('transform', `translate(${x}, ${y})`);

// slide the name and value away from the edges of the SVG if they extend outside the margin
adjustText(nameSpan, nameLength, x);
adjustText(valueSpan, valueLength, x);
});

svg.attr("viewBox", [
0,
(has.above ? -40 : 0) - margin.top,
width,
barHeight +
margin.top +
(has.above ? 40 : 0) +
(has.below ? 40 : 0) +
margin.bottom
]);

return svg.node();
}
Insert cell
size = width - margin.right - margin.left
Insert cell
barHeight = 40
Insert cell
margin = ({ top: 8, bottom: 8, left: 8, right: 8 })
Insert cell
// adapted from https://observablehq.com/@d3/donut-chart#data
data = [
{ name: "0", value: 1e5 },
{ name: "<10", value: 19912018 + 20501982 },
{ name: "10-19", value: 20679786 + 21354481 },
{ name: "20-29", value: 22604232 + 21698010 },
{ name: "30-39", value: 21183639 + 19855782 },
{ name: "40-49", value: 20796128 + 21370368 },
{ name: "50-59", value: 22525490 + 21001947 },
{ name: "60-69", value: 18415681 + 14547446 },
{ name: "70-79", value: 10587721 + 7730129 },
{ name: "≥80", value: 5811429 + 5938752 }
]
Insert cell
bars = makeBars(data)
Insert cell
getLength = {
const textElt = DOM.element("svg:text");
textElt.setAttribute("font-family", "sans-serif");
textElt.setAttribute("font-size", 12);

const view = svg`<svg
width="1"
height="1"
style="position: absolute; pointer-events: none"
>${textElt}</svg>`;
document.body.append(view);
return span => {
textElt.innerHTML = '';
textElt.append(span.cloneNode(true));
return textElt.getComputedTextLength();
};
}
Insert cell
makeBars = data => {
const total = data.reduce((acc, { value }) => acc + value, 0);
return data.reduce((acc, datum) => {
const prev = acc[acc.length - 1] || { pos: 0, size: 0 };
acc.push({
_: datum,
pos: prev.pos + prev.size,
size: datum.value / total
});
return acc;
}, []);
}
Insert cell
function insertIndicator(el, x, { orientation, showInnerLine, barIsDark }) {
const outsideLine = svg`<line x1=${x} x2=${x} stroke=black>`;
el.parentElement.append(outsideLine);

const insideLine = svg`<line x1=${x} x2=${x} stroke=${
barIsDark ? 'white' : 'black'
}>`;
// don’t overlap 1px-wide slices
if (showInnerLine) el.parentElement.append(insideLine);

// alternate above & below to reduce the chance of collisions
if (orientation === 'below') {
insideLine.setAttribute('y1', barHeight / 2);
insideLine.setAttribute('y2', barHeight);
outsideLine.setAttribute('y1', barHeight);
outsideLine.setAttribute('y2', barHeight + 10);
} else {
insideLine.setAttribute('y1', barHeight / 2);
insideLine.setAttribute('y2', 0);
outsideLine.setAttribute('y1', 0);
outsideLine.setAttribute('y2', -10);
}
}
Insert cell
function adjustText(span, length, x) {
if (x + length / 2 > width) {
span.setAttribute('x', width - x);
span.setAttribute('text-anchor', 'end');
} else if (x - length / 2 < 0) {
span.setAttribute('x', -x);
span.setAttribute('text-anchor', 'start');
}
}
Insert cell
Insert cell
function isDark(c) {
return contrast(c, 'black') * 0.75 < contrast(c, 'white');
}
Insert cell
color = (data.length > 8
? d3
.scaleOrdinal()
.range(
d3
.quantize(t => d3.interpolateTurbo(t * 0.8 + 0.2), data.length)
.reverse()
)
: d3.scaleOrdinal(d3.schemeAccent)
).domain(d3.shuffle(data.map(d => d.name)))
Insert cell
d3 = require('d3@6')
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more