Public
Edited
Mar 21, 2024
1 star
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
plot(settings)(marks)
Insert cell
Insert cell
Insert cell
Insert cell
settings = { //{width, height: ymax + 3 * step, step}
const color = "white";
const primer = "darkslategrey"; //"#7e9a9a",
const dimmed = lighten(50)(primer);
// const start = 5; // start of baselines
const leftMargin = ø(3);
const rightMargin = ø(1);
const contentX = ø(6);
return ({
step,
ymax,
start, // of baselines
width, //: ø(9*7) + leftMargin + rightMargin,
height: ymax + 3 * step,
leftMargin,
rightMargin,
topMargin: step,
x: { // x start position of each column
size: ø(1),
count: ø(4),
labels: contentX,
hooks: contentX,
},
dy: -4, // so hooks end up in a dot; 0 aligns with baseline
baseline: {show: true, stroke: "lightblue", strokeWidth: .2},
style,
})
}
Insert cell
// global style affects the plot as a whole
// as opposed to per level styles
styles = [
{
name: "Default",
},
{
name: "Sunny",
backgroundColor: "LemonChiffon",
color: "black",
},
{
name: "Black",
backgroundColor: "black",
color: "lightyellow",
},
{
name: "White on Dark Slate Grey",
backgroundColor: "darkslategray",
dimmed: lighten(50)("darkslategray"),
fontFamily: "sans-serif",
fontSize: 12,
color: "white",
},
{
name: "Serif White on Dark Slate Grey",
backgroundColor: "darkslategray",
dimmed: lighten(50)("darkslategray"),
fontFamily: "serif",
fontSize: 12,
color: "white",
},
]
Insert cell
ymax = d3.max(descendants.map(node => node.y))
Insert cell
Insert cell
columns = [
// { // uncomment these lines to also show the sizes (e.g. of the flare dataset)
// label: "Size",
// value: "size",
// format: d3.format(","),
// },
{
label: "Count",
value: "count",
format: (value, d) => d.children ? d3.format(",")(value) : null,
}
]
Insert cell
Insert cell
marks = [
baselines(settings),
hooks(settings)(links),
content(settings)(descendants),
headers(settings)(columns),
]
Insert cell
contents = [
labels(settings),
cols(settings),
]
Insert cell
Insert cell
Insert cell
flare = ({
title: "Flare ActionScript Library",
arguments: await FileAttachment("flare-2.json").json()
})
Insert cell
root = {
let y = 0;
return d3
.hierarchy(input.arguments)
.eachBefore(
(node) =>
{
node.style = input.nest && input.nest[node.depth];

// add Size and Count
node.size = node.copy().sum((node) => node.value).value;
node.count = node.copy().sum((node) => node.children ? 0 : 1).value;

if (node.depth === 0) node.i = 0; // root needs its index, too;
// index all children; needed below to pick the proper primer from palette
node.children?.forEach((child, i) => child.i = i);

node.depth === 0 && console.log(node);
node.primer = node.primer
|| node.style?.circleFill
|| Array.isArray(node.style?.palette)
&& node.style?.palette[node.i % node.style?.palette.length]
|| node.parent?.primer
;

// update y and add position
node.x = ø(node.depth);
node.y = (y += ø(node?.space || 1));
}
)
}
Insert cell
Insert cell
descendants = root.descendants()
Insert cell
Insert cell
Insert cell
plot =
(settings = {}) =>
(marks) =>
{
const {width, height, style} = settings;
const canvas = d3
.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("overflow", style?.overflow)
.style("background-color", style?.backgroundColor || "transparent")
.style("font-family", style?.fontFamily || "monospace")
.style("font-size", style?.fontSize || 10)
.style("fill", style?.color)
;
return marker({group: true})(settings)(canvas)(marks).node();
}
Insert cell
marker =
({group = false} = {}) => // group: use a <g> shifted down and right
({leftMargin = 0, topMargin = 0, x = 0} = {}) =>
(node) =>
(marks) =>
{
const g = group
? node
.append("g")
.attr("name", "marker")
.attr("transform", `translate(${[leftMargin, topMargin]})`)
: node
;
marks.forEach((mark) => g.call(mark)); // dress up the tree
return node;
}
Insert cell
baselines =
({
ymax,
start = 0,
step,
width,
baseline: {show = false, stroke = "blue", strokeWidth = .1},
leftMargin = 0,
rightMargin = 0
}) =>
(pad) =>
{
const w = (l) => `M0 ${ø(l) + start}H${width - leftMargin - rightMargin}`;
show && pad
.append("path")
.attr("name", "baselines")
.attr("stroke", stroke)
.attr("stroke-width", strokeWidth)
.attr("d", d3.range(ymax / step + 1).map(w))
;
}
Insert cell
hooks =
(settings) =>
(links) =>
(pad) =>
{
const hook = (
({step, dy} = {}) =>
({source, target}) => `M${source.depth * step + (settings.x?.hooks || 0)} ${source.y + dy}`
+ `V${target.y + dy}`
+ `h${step}`
)
(settings);
pad
.append("path")
.attr("name", "hooks")
.attr("fill", "none")
.attr("stroke", settings?.style?.dimmed || "grey")
.attr("d", links.map(hook))
;
}
Insert cell
content =
(settings) =>
(data) =>
(pad) =>
{
marker
({group: false})
(settings)
(
pad
.append("g")
.attr("name", "content")
.selectAll("g")
.data(data)
.join("g")
.attr("name", d => `${d.kind} x=${d.x}`)
.attr("transform", d => `translate(0,${d.y})`)
)
(contents)
}
Insert cell
headers =
(settings) =>
(columns) =>
(pad) =>
{
for (const {label, value, format} of columns) {
pad.append("text")
.attr("x", settings.x[value])
.attr("text-anchor", "end")
.attr("font-weight", "bold")
.attr("fill", settings.color)
.text(label)
;
}
}
Insert cell
cols = (
(columns) =>
({x, style:{dimmed}}) =>
(node) =>
{
for (const {label, value, format} of columns) {
node.append("text")
.attr("x", x[value] || 0)
.attr("text-anchor", "end")
.attr("fill", dimmed || "grey")
.text(d => format(d[value], d))
;
}
}
)
(columns)
Insert cell
titles =
(settings) =>
(node) =>
{
node
.append("title")
.text(d => d.ancestors().reverse().map(d => d.data.name).join("/"))
;
}
Insert cell
labels =
({r = 2, dy = -4, x:{labels}} = {}) =>
(node) =>
{
node
.append("text")
.text(d => d.data.name)
.attr("x", d => d.x + labels)
.attr("dx", ø())
.style("fill", d => d.style?.fontFill)
.style("font-weight", d => d.style?.fontWeight)
.style("font-size", d => d.style?.fontSize)
.style("font-variant", d => d.style?.fontVariant)
.style("text-transform", d => d.style?.textTransform)
.style("stroke", d => d.style?.textStroke)
.style("stroke-width", d => d.style?.textWidth)
;
node.append("circle")
.attr("fill", d => lighter(d.primer || "white"))
.attr("stroke", d => d.style?.circleStroke || d.primer || "black")
.attr("stroke-width", d => d.style?.circleStrokeWidth || 1)
.attr("r", d => d.style?.r || 3)
.attr("cx", d => d.x + labels)
.attr("cy", dy)
;
}
Insert cell
Insert cell
ø = ((step) => (n = 1) => n * (step))(step)
Insert cell
lighter = lighten(30)
Insert cell
lighten = (
(lightness) =>
(color) =>
{
const {l, c, h} = d3.lch(color);
return d3.lch(l + lightness, c, h);
}
)
Insert cell
Insert cell
import {forest} from "@martien/forest-for-the-trees"
Insert cell
import {fanpalette} from "@martien/fan-map-library"
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