Public
Edited
Jan 23, 2024
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// import { PieChart } from "@d3/pie-chart-component"

// Copyright 2018-2023 Observable, Inc.
// Released under the ISC license.
// https://observablehq.com/@d3/pie-chart
function PieChart(
data,
{
plot_title = "",
plot_subtitle = "",
plot_caption = "",
plot_CenteredTitle = "",
dy_CenteredTitle = 0,
label_textSize = 10,
name = ([x]) => x, // given d in data, returns the (ordinal) label
value = ([, y]) => y, // given d in data, returns the (quantitative) value
title, // given d in data, returns the title text
width = 640, // outer width, in pixels
height = 400, // outer height, in pixels
marginTop = 40,
marginLeft = 0,
innerRadius = 0, // inner radius of pie, in pixels (non-zero for donut)
outerRadius = Math.min(width, height) / 2, // outer radius of pie, in pixels
labelRadius = innerRadius * 0.2 + outerRadius * 0.8, // center radius of labels
format = ",", // a format specifier for values (in the label)
names, // array of names (the domain of the color scale)
colors, // array of colors for names
label_color,
stroke = innerRadius > 0 ? "none" : "white", // stroke separating widths
strokeWidth = 1, // width of stroke separating wedges
strokeLinejoin = "round", // line join of stroke separating wedges
padAngle = stroke === "none" ? 1 / outerRadius : 0 // angular separation between wedges, in radians
} = {}
) {
// Compute values.
const N = d3.map(data, name);
const V = d3.map(data, value);
const I = d3.range(N.length).filter((i) => !isNaN(V[i]));

// Unique the names.
if (names === undefined) names = N;
names = new d3.InternSet(names);

// Chose a default color scheme based on cardinality.
if (colors === undefined) colors = d3.schemeSpectral[names.size];
if (colors === undefined)
colors = d3.quantize(
(t) => d3.interpolateSpectral(t * 0.8 + 0.1),
names.size
);

// Construct scales.
const color = d3.scaleOrdinal(names, colors);

// Chose a default color scheme based on cardinality.
if (label_color === undefined) label_color = "black";

// Compute titles.
if (title === undefined) {
const formatValue = d3.format(format);
title = (i) => `${N[i]}\n${formatValue(V[i])}`;
} else {
const O = d3.map(data, (d) => d);
const T = title;
title = (i) => T(O[i], i, data);
}

// Construct arcs.
const arcs = d3
.pie()
.padAngle(padAngle)
.sort(null)
.value((i) => V[i])(I);
const arc = d3.arc().innerRadius(innerRadius).outerRadius(outerRadius);
const arcLabel = d3.arc().innerRadius(labelRadius).outerRadius(labelRadius);

const svg = d3
.create("svg")
.attr("width", width + marginLeft * 2)
.attr("height", height + marginTop * 3.5)
.attr("viewBox", [
-width / 2 - marginLeft,
-height / 2 - marginTop,
width + marginLeft,
height + marginTop
])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;");

svg
.append("g")
.attr("stroke", stroke)
.attr("stroke-width", strokeWidth)
.attr("stroke-linejoin", strokeLinejoin)
.selectAll("path")
.data(arcs)
.join("path")
.attr("fill", (d) => color(N[d.data]))
.attr("d", arc)
.append("title")
.text((d) => title(d.data));

svg
.append("g")
.attr("font-family", "sans-serif")
.attr("font-size", label_textSize)
.attr("text-anchor", "middle")
.selectAll("text")
.data(arcs)
.join("text")
.attr("transform", (d) => `translate(${arcLabel.centroid(d)})`)
.selectAll("tspan")
.data((d) => {
const lines = `${title(d.data)}`.split(/\n/);
return d.endAngle - d.startAngle > 0.25 ? lines : lines.slice(0, 1);
})
.join("tspan")
.attr("x", 0)
.attr("y", (_, i) => `${i * 1.1}em`)
.attr("font-weight", (_, i) => (i ? null : "bold"))
.attr("fill", label_color)
.text((d) => d);

function wrap(text, wrapWidth) {
text.each(function () {
var text = d3.select(this),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
lineNumber = 0,
lineHeight = 1.1, // ems
y = text.attr("y"),
dy = parseFloat(text.attr("dy")),
tspan = text
.text(null)
.append("tspan")
.attr("x", 0)
.attr("y", y)
.attr("dy", `${dy}em`);
while ((word = words.pop())) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > wrapWidth) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text
.append("tspan")
.attr("x", 0)
.attr("y", y)
.attr("dy", ++lineNumber * lineHeight + dy + "em")
.text(word);
}
}
});
return 0;
}

setTimeout(() => {
svg.selectAll("tspan").attr("dy", -1).attr("dx", 0).call(wrap, 80);
}, 0);

svg
.append("text")
.attr("x", -width / 2)
.attr("y", -height / 2 - marginTop)
.attr("font-weight", "700")
.attr("font-size", "20px")
.attr("font-family", "sans-serif")
.attr("line-height", "1.15")
.attr("fill", "black")
.text(plot_title);

svg
.append("text")
.attr("x", -width / 2)
.attr("y", -height / 2 - marginTop + 20)
.attr("font-weight", "300")
.attr("font-size", "17px")
.attr("font-family", "sans-serif")
.attr("line-height", "1.15")
.attr("fill", "thin")
.text(plot_subtitle);

svg
.append("text")
.attr("class", "caption")
.attr("y", height / 2 + marginTop / 2)
.attr("font-weight", "100")
.attr("font-size", "small")
.attr("font-family", "sans-serif")
.attr("line-height", "1.15")
.attr("fill", "#838383")
.text(plot_caption);

setTimeout(() => {
svg
.selectAll(".caption")
.attr("dy", 0)
.attr("dx", 0)
.call(wrap, width - marginLeft)
.attr("text-anchor", "middle");
}, 3);

svg
.append("text")
.attr("class", "CenteredTitle")
.attr("y", dy_CenteredTitle)
.attr("font-weight", "900")
.attr("font-size", "30px")
.attr("font-family", "sans-serif")
.attr("line-height", "1.15")
.attr("fill", "black")
.attr("text-anchor", "middle")
.text(plot_CenteredTitle);

setTimeout(() => {
svg
.selectAll(".CenteredTitle")
.attr("dy", 0)
.attr("dx", 0)
.call(wrap, 200)
.attr("text-anchor", "middle");
}, 3);

return Object.assign(svg.node(), { scales: { color } });
}
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