Public
Edited
Aug 1, 2024
Paused
8 forks
Importers
13 stars
Also listed in…
Visualization Concepts
Insert cell
Insert cell
chart = TreeValue(flare, {
value: (d) => d.size, // size of each node (file); null for internal nodes (folders)
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
})
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 TreeValue(
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
value = (d) => d.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 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
margin = 0, // 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 = 1, // cell padding, in pixels
round = false, // whether to round to exact pixels
colorScheme = d3.interpolateSpectral, // color scheme, if any
color = null, // a custom colorScale
fill = "#ccc", // fill for node rects (if no color encoding)
fillOpacity = 1, // 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, 25]
} = {}
) {
// 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);

// Compute the values of internal nodes by aggregating from the leaves.
value == null ? root.count() : root.sum((d) => Math.max(0, value(d)));

// 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) {
const colorScale = d3
.scaleSequential([0, root.children.length - 1], colorScheme)
.unknown(fill);
root.children.forEach((child, i) => (child.index = i));

// default color code by the parent's color
color = (d) => colorScale(d.ancestors().reverse()[1]?.index);
}

const descendants = root.descendants();

// TODO find a way to use a sqrt scale
// Radius scale
let rScale = d3
.scaleLinear()
.domain([0, d3.max(descendants, (d) => d?.value)])
.range([0.1, d3.max(descendants, (d) => (d.x1 - d.x0) / 2) - 1]);
let fontScale = d3.scaleLinear().domain([0, 20]).range(fontSizeRange);

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);
}

// 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")
.selectAll("a")
.data(descendants)
.join("a")
.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})`
);

cell
.append("circle")
.attr("r", (d) => (!d.children ? rScale(d.value) : parentR))
.attr("cy", (d) => rScale(d.value))
.attr("fill", (d) => (!d.children ? (color ? color(d) : fill) : "none"))
.attr("stroke", (d) => (d.children ? (color ? color(d) : fill) : "none"))
.attr("fill-opacity", fillOpacity);

const text = cell
.filter((d) => d.x1 - d.x0 > minHeightForLabel)
.append("text")
.attr("text-anchor", (d) => (d.children ? "end" : "start"))
.attr("x", (d) => (!d.children ? rScale(d.value) + 2 : -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], fontScale(rScale(d.value))))
.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));

return svg.node();
}
Insert cell
import {howto} from "@d3/example-components"
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