Public
Edited
Aug 6, 2024
Fork of Tree, Value
Importers
7 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
chart2 = TreeMultiValue(treeBudget, {
categories: budgetYears,
values: budgetYears.map((y) => (d) => d[y]),
label: (d) => d[0],
drawLayout: false,
width,
height: 600,
minHeightForGlyph: 0.01,
padding: 3,
format: "$0.2s",
color: d3.interpolateBlues,
drawBarValues: false,
drawBarAxis: false,
normalizeBars,
getChartGlyph: glyph==="Bars"? getBarChartGlyph : getCircleChartGlyph
// marginRight: 400
// drawLayout: true
})
Insert cell
budgetYears = d3.range(yearRange[0], yearRange[1] + 1)
Insert cell
budgetYears.map((y) => (d) => d[y]).map(f => f(budgetData[100]))
Insert cell
treeBudget = d3.rollup(
budgetData,
(v) =>
[Object.fromEntries(budgetYears.map((y) => [y, d3.sum(v.map((d) => d[y]))]))]
,
...groupBudgetBy.map((a) => (d) => d[a])
)
Insert cell
Insert cell
chart = TreeMultiValue(flare, {
values: [(d) => d.size], // size of each node (file); null for internal nodes (folders)
categories: ["value"],
label: (d) => d.name, // display name for each cell
title: (d, n) =>
`${n
.ancestors()
.reverse()
.map((d) => d.data.name)
.join(".")}\n${n.value.toLocaleString("en")}`, // hover text
link: (d, n) =>
n.children
? `https://github.com/prefuse/Flare/tree/master/flare/src/${n
.ancestors()
.reverse()
.map((d) => d.data.name)
.join("/")}`
: `https://github.com/prefuse/Flare/blob/master/flare/src/${n
.ancestors()
.reverse()
.map((d) => d.data.name)
.join("/")}.as`,
width: 1152,
height: 800,
getChartGlyph: glyph==="Bars"? getBarChartGlyph : getCircleChartGlyph
})
Insert cell
flare = FileAttachment("flare.json").json()
Insert cell
Insert cell
// Combines the code from icicle and tree examples
// Copyright 2022 John Alexis Guerra Gómez
// Copyright 2021 Observable, Inc.
// Released under the ISC license.
// https://observablehq.com/@john-guerra/tree-value

function TreeMultiValue(
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
format = ",", // format specifier string or function for values
categories = ["value"],
values = categories.map((c) => (d) => d[c]), // 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 node d, returns the name to display on the rectangle
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
marginTop = 0, // top margin, in pixels
marginRight = 100, // right margin, in pixels
marginBottom = 0, // bottom margin, in pixels
marginLeft = 0, // left margin, in pixels
padding = 1, // cell padding, in pixels
round = false, // whether to round to exact pixels
color = d3.schemeCategory10, // color scheme, if any
fill = "#ccc", // fill for node rects (if no color encoding)
fillOpacity = 0.8, // fill opacity for node rects
stroke = "#555", // stroke for links
strokeWidth = 1.5, // stroke width for links
strokeOpacity = 0.4, // stroke opacity for links
strokeLinejoin, // stroke line join for links
strokeLinecap, // stroke line cap for links
halo = "#fff", // color of label halo
haloWidth = 3, // padding around the labels
parentR = 3, // Radius for inner nodes
drawLayout = false,
minHeightForLabel = 1,
fontSizeRange = [1, 18],
fontSizeDomain = [1, 40], //
minHeightForGlyph = 1,
colorScaleTitle = "Color Scale",
drawBarAxis = true,
drawBarValues = true,
normalizeBars = true, // If true the height will be adjusted per cell
getChartGlyph = getBarChartGlyph
} = {}
) {
// 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.
let root =
path != null
? d3.stratify().path(path)(data)
: id != null || parentId != null
? d3.stratify().id(id).parentId(parentId)(data)
: d3.hierarchy(data, children).sort(sort);

// value == null ? root.count() : root.sum(d => Math.max(0, value(d)));

// Compute the values of internal nodes by aggregating from the leaves
// the space necessary for the bars, i.e. possitive + negative bar sizes
values === null
? root.count()
: root.sum((d) => {
const [min, max] = d3.extent(values.map((v) => v(d)));
const maxNegBar = min < 0 ? 0 - min : 0;
const maxPosBar = max > 0 ? max - 0 : 0;
return maxNegBar + maxPosBar;
});

// Compute formats.
if (typeof format !== "function") format = d3.format(format);

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

// Compute the partition layout. Note that x and y are swapped!
d3
.partition()
.size([height - marginTop - marginBottom, width - marginLeft - marginRight])
.padding(padding)
.round(round)(root);

// Construct a color scale.
if (color != null) {
if (typeof color == "function") {
color = d3.quantize(color, categories.length);
}
color = d3.scaleOrdinal(color).domain(categories);
}

const descendants = root.descendants();
const leaves = root.leaves();

const maxLeafHeight = d3.max(leaves, (d) => d.x1 - d.x0);
const maxLeafWidth = d3.max(leaves, (d) => (d.y1 - d.y0) / 2);

leaves.forEach((d, i) => {
d._valueExtent = d3.extent(values.map((v) => v(d.data)));
d.i = i;
});
const valueExtent = d3.extent(
// values.map((v) => d3.extent(leaves, (d) => v(d.data))).flat()
leaves.map((d) => d._valueExtent).flat()
);

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

// Draw the Icicle
if (drawLayout) {
svg
.append("g")
.selectAll("rect")
.data(descendants)
.join("rect")
.attr("y", (d) => d.x0)
.attr("x", (d) => d.y0)
.attr("height", (d) => d.x1 - d.x0)
.attr("width", (d) => d.y1 - d.y0)
.attr("fill", "none")
.attr("stroke", stroke)
.attr("stroke-opacity", strokeOpacity)
.attr("stroke-linecap", strokeLinecap)
.attr("stroke-linejoin", strokeLinejoin)
.attr("stroke-width", strokeWidth);
}

console.log("links", root.links());
// Links
svg
.append("g")
.attr("fill", "none")
.attr("stroke", stroke)
.attr("stroke-opacity", strokeOpacity)
.attr("stroke-linecap", strokeLinecap)
.attr("stroke-linejoin", strokeLinejoin)
.attr("stroke-width", strokeWidth)
.selectAll("path")
.data(root.links())
.join("path")
.attr(
"d",
d3
.linkHorizontal()
.x((d) => d.y0 + (d.y1 - d.y0) / 2)
.y((d) => d.x0 + (d.x1 - d.x0) / 2)
);

const cell = svg
.append("g")
.attr("class", "cells")
.selectAll("a")
.data(descendants)
.join("a")
.attr("class", "cell")
.attr("xlink:href", link == null ? null : (d) => link(d.data, d))
.attr("target", link == null ? null : linkTarget)
.attr(
"transform",
(d) => `translate(${d.y0 + +(d.y1 - d.y0) / 2},${d.x0})`
);

let xScale = d3
.scaleBand()
.padding(0.2)
.domain(categories)
.range([0.1, maxLeafWidth - 1]);

let yScale = d3
.scaleLinear()
.domain([Math.min(valueExtent[0], 0), Math.max(valueExtent[1], 0)])
.range([maxLeafHeight - 1, 1]);

let fontScale = d3.scaleLinear().domain(fontSizeDomain).range(fontSizeRange);

cell.each(function (d) {
if (d.children || d.x1 - d.x0 < minHeightForGlyph) return;
// console.log("cell.each", d, xScale, yScale);

const localData = values.map((v, i) => ({
i,
value: v(d.data),
d: d,
category: categories[i]
}));

getChartGlyph(d3.select(this), localData, {
x: xScale,
y: yScale,
font: fontScale,
color: (d) => color(d.category),
xAttr: (d) => d.category,
yAttr: (d) => d.value,
valueExtent: normalizeBars ? d._valueExtent : null,
halo,
haloWidth,
d,
format,
drawBarAxis,
drawBarValues,
fontSizeRange,
fillOpacity
});
});

const text = cell
.filter((d) => d.x1 - d.x0 > minHeightForLabel)
.append("text")
.attr("text-anchor", "end")
// .attr("text-anchor", (d) => (d.children ? "end" : "start"))
.attr("x", (d) => (!d.children ? -10 : -parentR - 2))
.attr("y", (d) => (d.x1 - d.x0) / 2)
.attr("paint-order", "stroke")
.attr("stroke", halo)
.attr("stroke-width", haloWidth)

.attr("font-size", (d) =>
Math.min(
fontSizeRange[1],
Math.min(fontSizeRange[1], fontScale(d.x1 - d.x0))
)
)
.attr("dy", "0.32em");

if (label != null) text.append("tspan").text((d) => label(d.data, d));

// text
// .append("tspan")
// .attr("fill-opacity", 0.7)
// .attr("dx", label == null ? null : 3)
// .text((d) => format(d.value));

if (title != null) cell.append("title").text((d) => title(d.data, d));

colorLegend(
svg
.append("g")
.attr("class", "color-legend")
.attr("fill-opacity", fillOpacity)
.attr(
"transform",
`translate(${width - marginLeft - marginRight + 20}, ${marginTop + 20})`
),
color,
{ title: colorScaleTitle }
);

svg.node();
const target = htl.html`<div>
<style>
.cell .border {
display:none;
}
.cell:hover .border {
display:block;
}
</style>
${svg.node()}</div>`;
target.root = root;
return target;
}
Insert cell
function getBarChartGlyph(
selection,
data,
{
offsetY = 0,
offsetX = 0,
xAttr = (d) => d.x,
yAttr = (d) => d.y,
color = d3.scaleOrdinal(d3.schemeCategory10),
x = d3.scaleBand(),
y = d3.scaleLinear(),
valueExtent = null,
d = null,
halo = "#fff", // color of label halo
haloWidth = 3, // padding around the labels
format = d3.format("0.2d"),
drawBarAxis = true,
drawBarValues = true,
fontSizeRange = [1, 25],
font = d3.scaleLinear().domain([0, 20]).range([0, 20]),
fillOpacity = 1,
} = {}
) {
console.log("getBarChartGlyph", d.i, offsetY, y.range(), d);

const margin = 1;

const lengthCell = d.x1 - d.x0;
const lengthRange = y.range()[0] - y.range()[1];
const maxBarSize = y(0) - y(d.value); // the value of the largest bar
if (valueExtent) {
y.domain([Math.min(d._valueExtent[0], 0), Math.max(d._valueExtent[1], 0)]);
y.range([d.x1 - d.x0 - margin, margin]);
offsetY = 0;
} else {
offsetY = lengthCell / 2 - lengthRange / 2;
}

if (valueExtent) {
selection
.append("rect")
.attr("class", (d) => `outline${d.i}`)
.attr("x", 0)
.attr("y", 0)
.attr("height", y.range()[0] - y.range()[1])
.attr("width", x.range()[1])
.attr("fill", "none")
.attr("stroke", "#7777")
.style("rx", 2);
}

// Add a rect for each bar.
selection
// .append("g")
.selectAll()
.data(data)
.join("rect")
.attr("fill-opacity", fillOpacity)
.attr("x", (d) => x(xAttr(d)) + offsetX)
.attr("y", (d) => (yAttr(d) < 0 ? y(0) : y(yAttr(d))) + offsetY)
.attr("height", (d) => y(0) - y(yAttr(d) < 0 ? -yAttr(d) : yAttr(d)) + 1)
.attr("width", x.bandwidth())
.attr("value", (d) => yAttr(d))
.attr("fill", (d) => color(d))
.append("title")
.text((d) => `${d.category}: ${format(yAttr(d))}`);

// zero line
selection
.append("line")
.attr("class", (d) => `outline${d.i}`)
.attr("x1", 0)
.attr("x2", x.range()[1])
.attr("y1", y(0) + offsetY)
.attr("y2", y(0) + offsetY)
.attr("fill", "none")
.attr("stroke", "#7777")
.attr("stroke-dasharray", "4 4");

if (drawBarValues) {
// Bar value label
selection
.selectAll()
.data(data)
.join("text")
.attr("x", (d) => x(xAttr(d)) + x.bandwidth() / 2 + offsetX)
.attr("y", (d) => y(yAttr(d)) + offsetY)
.attr("text-anchor", "middle")
.attr("paint-order", "stroke")
.attr("stroke", halo)
.attr("stroke-width", haloWidth)
.attr("dy", "0.32em")
.text((d) => yAttr(d) && format(yAttr(d)));
}

if (drawBarAxis) {
// Add the x-axis and label.
selection
.append("g")
.attr("class", "border")
.attr("transform", `translate(${offsetX},${y.range()[0] + offsetY})`)
.call(d3.axisBottom(x).tickSizeOuter(0));

// Add the y-axis and label, and remove the domain line.
selection
.append("g")
.attr("class", "border")
.attr("transform", `translate(${offsetX},${offsetY})`)
.call(
d3
.axisLeft(y)
.ticks(Math.floor(lengthCell / 15))
.tickFormat(format)
);
// .call((g) => g.select(".domain").remove())
// .call((g) =>
// g
// .append("text")
// .attr("x", -marginLeft)
// .attr("y", 10)
// .attr("fill", "currentColor")
// .attr("text-anchor", "start")
// .text("")
// );
}

const debug = false;

if (debug) {
selection
.append("circle")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", 5)
.attr("fill", "pink");

selection
.append("text")
.attr("x", 0)
.attr("y", 0)
.text((d) => d.i);

selection
.append("text")
.attr("x", 10)
.attr("y", offsetY)
.attr("fill", "lightblue")
.text((d) => d.i);

selection
.append("circle")
.attr("cx", 10)
.attr("cy", y(0))
.attr("r", 5)
.attr("fill", "lightgreen");

selection
.append("text")
.attr("x", 10)
.attr("y", y(0))
.text((d) => d.i);
// .attr("fill", "lightgreen");

selection
.append("circle")
.attr("cx", 20)
.attr("cy", offsetY)
.attr("r", 5)
.attr("fill", "lightblue");

selection
.append("rect")
// .attr("class", (d) => `outline${d.i}`)
.attr("x", 0)
.attr("y", offsetY)
.attr("height", y.range()[0])
.attr("width", x.range()[1])
.attr("fill", "none")
.attr("stroke", "lime")
.style("rx", 2);
}

return selection.node();
}
Insert cell
function getCircleChartGlyph(
selection,
data,
{
offsetY = 0,
offsetX = 0,
xAttr = (d) => d.x,
yAttr = (d) => d.y,
color = d3.scaleOrdinal(d3.schemeCategory10),
x = d3.scaleBand(),
y = d3.scaleLinear(),
valueExtent = null,
d = null,
halo = "#fff", // color of label halo
haloWidth = 3, // padding around the labels
format = d3.format("0.2d"),
drawBarAxis = true,
drawBarValues = true,
rFactor = 1 // how many times the bandwith can the range be
} = {}
) {
// console.log("getBarChartGlyph", d.i, offsetY, y.range(), d);

const margin = 1;
const rScale = d3.scaleRadial();
rScale.domain(y.domain());
rScale.range([0, x.bandwidth()*rFactor]);

const lengthCell = d.x1 - d.x0;
const lengthRange = y.range()[0] - y.range()[1];
const maxBarSize = y(0) - y(d.value); // the value of the largest bar
if (valueExtent) {
y.domain([Math.min(d._valueExtent[0], 0), Math.max(d._valueExtent[1], 0)]);
y.range([d.x1 - d.x0 - margin, margin]);
offsetY = 0;

rScale.domain(y.domain());
offsetY = 0;
} else {
offsetY = lengthCell / 2 - lengthRange / 2;
}

// if (valueExtent) {
// selection
// .append("rect")
// .attr("class", (d) => `outline${d.i}`)
// .attr("x", 0)
// .attr("y", 0)
// .attr("height", y.range()[0] - y.range()[1])
// .attr("width", x.range()[1])
// .attr("fill", "none")
// .attr("stroke", "#7777")
// .style("rx", 2);
// }

// Add a rect for each bar.
selection
// .append("g")
.selectAll()
.data(data)
.join("circle")
.attr("cx", (d) => x(xAttr(d)) + x.bandwidth() / 2 + offsetX)
.attr("cy", lengthCell / 2)
.attr("r", (d) => rScale(yAttr(d)))
// .attr("width", x.bandwidth())
.attr("value", (d) => yAttr(d))
.attr("fill", (d) => color(d))
.attr("opacity", 0.3)
.append("title")
.text((d) => `${d.category}: ${format(yAttr(d))}`);

// // zero line
// selection
// .append("line")
// .attr("class", (d) => `outline${d.i}`)
// .attr("x1", 0)
// .attr("x2", x.range()[1])
// .attr("y1", y(0) + offsetY)
// .attr("y2", y(0) + offsetY)
// .attr("fill", "none")
// .attr("stroke", "#7777")
// .attr("stroke-dasharray", "4 4");

if (drawBarValues) {
// Bar value label
selection
.selectAll()
.data(data)
.join("text")
.attr("x", (d) => x(xAttr(d)) + x.bandwidth() / 2 + offsetX)
.attr("y", (d) => lengthCell / 2)
.attr("text-anchor", "middle")
.attr("paint-order", "stroke")
.attr("stroke", halo)
.attr("stroke-width", haloWidth)
.attr("dy", "0.32em")
.text((d) => yAttr(d) && format(yAttr(d)));
}

// if (drawBarAxis) {
// // Add the x-axis and label.
// selection
// .append("g")
// .attr("class", "border")
// .attr("transform", `translate(${offsetX},${y.range()[0] + offsetY})`)
// .call(d3.axisBottom(x).tickSizeOuter(0));

// // Add the y-axis and label, and remove the domain line.
// selection
// .append("g")
// .attr("class", "border")
// .attr("transform", `translate(${offsetX},${offsetY})`)
// .call(
// d3
// .axisLeft(y)
// .ticks(Math.floor(lengthCell / 15))
// .tickFormat(format)
// );
// // .call((g) => g.select(".domain").remove())
// // .call((g) =>
// // g
// // .append("text")
// // .attr("x", -marginLeft)
// // .attr("y", 10)
// // .attr("fill", "currentColor")
// // .attr("text-anchor", "start")
// // .text("")
// // );
// }

const debug = false;

if (debug) {
selection
.append("circle")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", 5)
.attr("fill", "pink");

selection
.append("text")
.attr("x", 0)
.attr("y", 0)
.text((d) => d.i);

selection
.append("text")
.attr("x", 10)
.attr("y", offsetY)
.attr("fill", "lightblue")
.text((d) => d.i);

selection
.append("circle")
.attr("cx", 10)
.attr("cy", y(0))
.attr("r", 5)
.attr("fill", "lightgreen");

selection
.append("text")
.attr("x", 10)
.attr("y", y(0))
.text((d) => d.i);
// .attr("fill", "lightgreen");

selection
.append("circle")
.attr("cx", 20)
.attr("cy", offsetY)
.attr("r", 5)
.attr("fill", "lightblue");

selection
.append("rect")
// .attr("class", (d) => `outline${d.i}`)
.attr("x", 0)
.attr("y", offsetY)
.attr("height", y.range()[0])
.attr("width", x.range()[1])
.attr("fill", "none")
.attr("stroke", "lime")
.style("rx", 2);
}

return selection.node();
}
Insert cell
function colorLegend(
selection,
colorScale,
{ height = null, itemWidth = 20, padding = 0.2, title = "Color Scale" } = {}
) {
height = height || colorScale.domain().length * itemWidth * (1 + padding);
const y = d3
.scaleBand()
.domain(colorScale.domain())
.padding(padding)
.range([0, height]);

selection.append("text").text(title).attr("font-weight", "bolder");
selection
.append("g")
.selectAll("g.legendCell")
.data(colorScale.domain())
.join("g")
.attr("transform", (d) => `translate(0,${y(d)})`)
.call((cell) =>
cell
.append("rect")
.attr("width", itemWidth)
.attr("height", itemWidth)
.attr("fill", colorScale)
)
.call((cell) =>
cell
.append("text")
.text((d) => d)
.attr("text-anchor", "start")
.attr("y", +itemWidth / 2)
.attr("dy", "0.32em")
.attr("dx", itemWidth + 2)
);
}
Insert cell
import {howto} from "@d3/example-components"
Insert cell
import {outlays as budgetData} from "@john-guerra/us-federal-budget-outlays"
Insert cell
classifications = FileAttachment("classifications.json").json()
Insert cell
taxonomies = ["Few Shot Tot", "LLM Fewshot COT Classification", "Zero Shot"]
Insert cell
import { multiAutoSelect } from "@john-guerra/multi-auto-select"
Insert cell
import {interval} from '@mootari/range-slider'
Insert cell
import {Icicle} from "@d3/icicle"
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