Public
Edited
Jun 8, 2023
3 forks
19 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// Make a tree
makePlot = (letter) =>
addTooltips(
Plot.plot({
marks: [
Plot.frame({ stroke: "#f3f3f3" }),
Plot.tree(
// Feels like there's gotta be a better way to do this sort, but I couldn't figure it out
makeTreeData(topN.filter((d) => d.firstLetter === letter)).data.sort(
(a, b) => {
// Get the total value of all of the child nodes to compare two nodes
let aValue = 0;
a.eachAfter((dd) => (aValue += dd.data?.value || 0));
let bValue = 0;
b.eachAfter((dd) => (bValue += dd.data?.value || 0));
return d3.ascending(bValue, aValue);
}
),
{
path: (d) => d.id,
r: 0, // no dots
// If someone types in, show that as the first piece of text
text: (d) =>
d.depth === 0 && search.length
? search.toUpperCase()
: nameof(d.id),
// Get counts of all children for the tooltip
title: (d) => {
let text = d.id.replaceAll("/", "");
d.eachAfter((dd) => {
if (dd.data?.name)
text = text.concat(
`\n ${letter}${nameof(dd.data.name)} (${d3.format(",")(
dd.data.value
)})`
);
});
return text;
},
strokeWidth: 1,
stroke: "#d3d3d3",
fontSize: (d) => {
// Again, feels like there's gotta be a better way to get the value
let value = 0;
const sum = d.copy().sum((dd) => dd?.value);
d.eachAfter((dd) => (value += dd.data?.value || 0));
return allFontScale(value);
}
}
)
],
axis: null,
width: w,
height,
margin: 5,
inset: 20,
insetRight: 50
}),
{ stroke: "black", "stroke-width": "1px" } // tooltip styles
)
Insert cell
w = width < 500 || search.length ? width : Math.floor(width / 3)
Insert cell
height = width < 500 || search.length ? width / 2 : Math.floor(width / 3)
Insert cell
// d.data.data has a pretty bad code smell :eek:
extent = d3.extent(makeTreeData(topN).leaves(), (d) => d.data.data.value)
Insert cell
allFontScale = d3.scaleLinear().domain(extent).range([6, 18])
Insert cell
Insert cell
candian_baby_names.csv
Type Table, then Shift-Enter. Ctrl-space for more options.

Insert cell
// Top n by first letter
topN = aq
.from(candian_baby_names)
.params({sex, n, search: search.toUpperCase()})
.rename({"First name at birth": "name", VALUE:"value"})
.filter((d) => d.Indicator === "Frequency")
.filter((d) => sex === "Any" ? true : d["Sex at birth"] === sex)
.filter((d) => aq.op.startswith(d.name, search))
.derive({
firstLetter: (d) => d.name[0]
})
.groupby("name", "firstLetter")
.rollup({value: d => aq.op.sum(d.value)}) // sum across sexes
.ungroup()
.select(["name", "firstLetter", "value"])
.groupby("firstLetter")
.orderby(aq.desc("value"))
.filter((d) => aq.op.rank() < n)
.ungroup()
.objects()
Insert cell
// Letter to display in small multiples
letters = search.length ? [search[0].toUpperCase()] : [ ...new Set(topN.map(d => d.firstLetter).sort())]
Insert cell
// Feels like there's a better way to do this...
makeTreeData = (data) => {
data.sort((a, b) => d3.ascending(a.name, b.name));
// Thanks to this contribution for the slicing up! https://observablehq.com/d/2f4c5a6620604d91
let prefixes = new Set();
prefixes.add(data[0].firstLetter);
let prevPrefix = data[0].firstLetter;
let i = 0;
for (let i = 0; i < data.length; i++) {
let datum = data[i];
let com = commonPrefix(prevPrefix, datum.name);
if (com == datum.firstLetter && i < data.length - 1) {
// peak
com = commonPrefix(data[i + 1].name, datum.name);
}
prevPrefix = data[i].name;
prefixes.add(com);
}
prefixes = [...prefixes].sort((a, b) => b.length - a.length);

const sliceUp = (str) => {
let pieces = [];
for (let prefix of prefixes) {
if (str.startsWith(prefix)) {
pieces.push(str.slice(prefix.length));
str = prefix;
}
}
pieces.push(str);
return pieces.reverse().join("/");
};

// Create statification (d3.stratify will interpret slash separation by default)
const stratified = d3.stratify().path((d) => sliceUp(d.name))(data);
// Create a hierarchy of the startified data
const root = d3.hierarchy(stratified);
// Return the summed values across the hierarchy
return root.copy().sum((d) => d.data?.value);
// .sort((a, b) => d3.ascending(b.value ?? b.data?.value, a.value ?? a.data?.value)); // doesn't work?
}
Insert cell
commonPrefix = (a,b) => {
let prefix = "";
for (let i = 0; i < Math.min(a.length, b.length); i++) {
if (a[i] == b[i]) prefix += a[i];
else break;
}
return prefix;
}
Insert cell
// Modified from: github.com/observablehq/plot/blob/1f790877e680b01e757fc9ea2aae4564ff621030/src/transforms/tree.js#LL257C1-L263C1
// Walk backwards to find the first slash.
function nameof(path) {
let i = path.length;
while (--i > 0) if (path[i] === "/") break;
return path.slice(i + 1);
}
Insert cell
import {addTooltips} from "@mkfreeman/plot-tooltip"
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more