Public
Edited
Jan 1, 2024
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
circlepacking = Pack(grouped_map, {
value: ([, value]) => value,
label: ([key]) => key?.split(/\s+/g).join("\n"),
title: ([key], n) =>
`${
n.depth
? `${n
.ancestors()
.reverse()
.slice(1)
.map(({ data: [key] }) => key)
.join("\n")}\n`
: ""
}${n.value.toLocaleString("en")}`,
sort: (a, b) => d3.descending(a.value, b.value),
padding: 30,
width: 2000,
height: 2000
})
Insert cell
// Copyright 2021-2023 Observable, Inc.
// Released under the ISC license.
// https://observablehq.com/@d3/pack
function Pack(data, { // data is either tabular (array of objects) or hierarchy (nested objects)
path, // as an alternative to id and parentId, returns an array identifier, imputing internal nodes
id = Array.isArray(data) ? d => d.id : null, // if tabular data, given a d in data, returns a unique identifier (string)
parentId = Array.isArray(data) ? d => d.parentId : null, // if tabular data, given a node d, returns its parent’s identifier
children, // if hierarchical data, given a d in data, returns its children
value, // given a node d, returns a quantitative value (for area encoding; null for count)
sort = (a, b) => d3.descending(a.value, b.value), // how to sort nodes prior to layout
label, // given a leaf node d, returns the display name
title, // given a node d, returns its hover text
link, // given a node d, its link (if any)
linkTarget = "_blank", // the target attribute for links, if any
width = 640, // outer width, in pixels
height = 400, // outer height, in pixels
margin = 1, // shorthand for margins
marginTop = margin, // top margin, in pixels
marginRight = margin, // right margin, in pixels
marginBottom = margin, // bottom margin, in pixels
marginLeft = margin, // left margin, in pixels
padding = 3, // separation between circles
fill = "#ddd", // fill for leaf circles
fillOpacity, // fill opacity for leaf circles
stroke = "#bbb", // stroke for internal circles
strokeWidth, // stroke width for internal circles
strokeOpacity, // stroke opacity for internal circles
} = {}) {

// If id and parentId options are specified, or the path option, use d3.stratify
// to convert tabular data to a hierarchy; otherwise we assume that the data is
// specified as an object {children} with nested objects (a.k.a. the “flare.json”
// format), and use d3.hierarchy.
const root = path != null ? d3.stratify().path(path)(data)
: id != null || parentId != null ? d3.stratify().id(id).parentId(parentId)(data)
: d3.hierarchy(data, children);
console.log(root)
// Compute the values of internal nodes by aggregating from the leaves.
value == null ? root.count() : root.sum(d => Math.max(0, value(d)));

// Compute labels and titles.
const descendants = root.descendants();
const leaves = descendants.filter(d => !d.children);
leaves.forEach((d, i) => d.index = i);
const L = label == null ? null : leaves.map(d => label(d.data, d));
const T = title == null ? null : descendants.map(d => title(d.data, d));

// Sort the leaves (typically by descending value for a pleasing layout).
if (sort != null) root.sort(sort);

// Compute the layout.
d3.pack()
.size([width - marginLeft - marginRight, height - marginTop - marginBottom])
.padding(padding)
(root);

const svg = d3.create("svg")
.attr("viewBox", [-marginLeft, -marginTop, width, height])
.attr("width", width)
.attr("height", height)
.attr("style", "max-width: 100%; height: auto; height: intrinsic;")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.attr("text-anchor", "middle");

const node = svg.selectAll("a")
.data(descendants)
.join("a")
.attr("xlink:href", link == null ? null : (d, i) => link(d.data, d))
.attr("target", link == null ? null : linkTarget)
.attr("transform", d => `translate(${d.x},${d.y})`);

node.append("circle")
.attr("fill", d => d.children ? "#fff" : fill)
.attr("fill-opacity", d => d.children ? null : fillOpacity)
.attr("stroke", d => d.children ? stroke : null)
.attr("stroke-width", d => d.children ? strokeWidth : null)
.attr("stroke-opacity", d => d.children ? strokeOpacity : null)
.attr("r", d => d.r);

if (T) node.append("title").text((d, i) => T[i]);

if (L) {
// A unique identifier for clip paths (to avoid conflicts).
const uid = `O-${Math.random().toString(16).slice(2)}`;

const leaf = node
.filter(d => !d.children && d.r > 10 && L[d.index] != null);

leaf.append("clipPath")
.attr("id", d => `${uid}-clip-${d.index}`)
.append("circle")
.attr("r", d => d.r);

leaf.append("text")
.attr("clip-path", d => `url(${new URL(`#${uid}-clip-${d.index}`, location)})`)
.selectAll("tspan")
.data(d => `${L[d.index]}`.split(/\n/g))
.join("tspan")
.attr("x", 0)
.attr("y", (d, i, D) => `${(i - D.length / 2) + 0.85}em`)
.attr("fill-opacity", (d, i, D) => i === D.length - 1 ? 0.7 : null)
.text(d => d);
}

return svg.node();
}
Insert cell
chart = {
const context = DOM.context2d(width, height);
context.translate(200, 200);
const circlesPerGroup = {}; // store in object with keys = the gorups
for (const group of groups) {
circlesPerGroup[group] = d3.packSiblings(
d3
.range(circles.filter((circle) => circle.group === group).length)
.map(() => ({
r: subgroupRadius + padding,
group: group
}))
);
}
const xOffset = 500; // how far right to move horizontally from initial position
const yOffsetPerGroup = 350; // this can also be defined with a scale if we have more groups
let movedCircles = [];
for (const [group, data] of Object.entries(circlesPerGroup)) {
const groupNumber = groups.indexOf(group);
data.forEach((circle, index) => {
movedCircles.push({
x: data[index].x + xOffset,
y: data[index].y + yOffsetPerGroup * groupNumber,
fill: circle.group === "NFL" ? "#FC803B" : "#1B81F7", // can be done with a colour scheme
group: circle.group
});
});
}

const tl = gsap.to(circles, {
x: (index, target, targets) => movedCircles[index].x,
y: (index, target, targets) => movedCircles[index].y,
fill: (index, target, targets) => movedCircles[index].fill,
duration: 1,
ease: "power.3.out",
stagger: { amount: 2 },
onUpdate: animate
});
tl.pause(); // pause to begin with; play if button is clicked

// this is what happens on update of the gsap animation
function animate() {
context.fillStyle = "white";
context.fillRect(-200, -200, width, height); // make sure to clear or fill the rect
for (const { x, y, r, fill } of circles) {
context.beginPath();
context.arc(x, y, r - padding, 0, 2 * Math.PI);
context.fillStyle = fill;
context.fill();
}
}

// run the drawing logic once just so that we can see the initial circle packing
animate();
tl.play();
return context.canvas;
}
Insert cell
gsap = gs.gsap
Insert cell
Insert cell
gs = require("https://cdnjs.cloudflare.com/ajax/libs/gsap/3.6.1/gsap.min.js")
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