Public
Edited
Mar 18, 2024
1 fork
1 star
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
import {forest} from "@martien/forest-for-the-trees"
Insert cell
datasets = ({
// "Agile Adoption": {
// raw: agileAdoption,
// nest: ["probe", "vote", "category", "facet", "prompt"],
// },
"Gemengde Scholen": {
raw: gemengdeScholen.arguments,
nest: ["probe", "vote", "category", "facet", "prompt"],
},
// "Analog versus Digital signal": {
// raw: analogVsDigital.arguments,
// nest: analogVsDigital.nests,
// }
})
Insert cell
schemes = ({
"Transport for London": schemeTransportForLondonLines,
"Category 10": d3.schemeCategory10,
"Accent": d3.schemeAccent,
"Dark2": d3.schemeDark2,
"Tableau 10": d3.schemeTableau10,
})
Insert cell
curves = ({
"Bump X": d3.curveBumpX,
"Step": d3.curveStep,
})
Insert cell
curve = curves[curver]
Insert cell
range = rotate(r)(schemes[scheme])
Insert cell
raw = source
// raw = datasets[source]
Insert cell
R.keys(raw.nest)
Insert cell
data = R.pipe(
d3.hierarchy,
tree,
dress(R.keys(raw.nest)),
)
(raw.arguments)
Insert cell
tree = d3
.tree()
.nodeSize([p(dx), M(1.7)])
.separation((a, b) => a.parent === b.parent ? 1 : 4)
Insert cell
dress = (nest) => (tree) => {

const bbox = getBBox(facetFont);
let category = null;
let side = null;

tree.x = altitude; // moves origin vertically
return tree
.eachBefore(
(node) => {
node.kind = nest[node.depth % nest.length];

if (isVote(node)) side = 1 - +(node.data.name === "Tegen") * 2, category = null;
node.side = side;

node.y = node.side * distances[node.depth % distances.length];
if (isCategory(node)) category = node.data.name;
node.category = category;

if (isFacet(node)) node.bbox = bbox(node.data.name);
}
)
.eachAfter((node) => {
if (node.depth > 0) { // vertically center left and right side around root
node.x += node.side * tree.children[1].x;
}
})
}
Insert cell
Insert cell
distances = R.pipe(
running,
R.map(p),
R.map(k => 12 * k)
)
([0,1,2,1,1/2])
Insert cell
dx = 7
Insert cell
procon = ({h = 124, range} = {}) => (data) =>
{
const links = R.map(addCategoryToTrack)(data.links());
const areVoteLinks = R.filter(linkIsVote)(links);
const butVoteLinks = R.reject(linkIsVote)(links);

const descendants = data.descendants();
const domain = uniques("category")(descendants);
const categories = R.filter(isCategory)(descendants);
const facets = R.filter(isFacet)(descendants);
const votes = R.filter(isVote)(descendants);
const butVotes = R.reject(isVote)(descendants);
const prompts = R.filter(isPrompt)(descendants);

const margin = 1;

return Plot.plot(
{
// each marker() yearns for settings{} first, and data[] second
marks: [

tracks({curve, stroke: "grey"})(areVoteLinks),
tracks({curve, stroke: "category"})(butVoteLinks),

categoryBoxes()(categories),
facetBoxes()(facets),
// BBoxes()(facets),
promptBoxes()(facets),
voteBoxes({size: 27})(votes),

stations()(butVotes),

voteTexts()(votes),
facetTexts({side: +1, margin})(facets),
facetTexts({side: -1, margin})(facets),
promptTexts({side: -1})(prompts),
promptTexts({side: +1})(prompts),
categoryTexts({width: 30})(categories),

],

x: {
inset: µ(1),
label: null,
// domain: [-M(w), +M(w)]
tickFormat: null, tickSize: null,
},

y: {
label: null,
domain: [+µ(h), -µ(h)],
tickFormat: null, tickSize: null,
},

style: { backgroundColor: "snow"},
// grid: true,
width,
height: width * 9 / 16,
marginRight: 0,
marginLeft: 0,
marginBottom: 0,
marginTop: 0,
color: {type: "categorical", domain, range},
}
)
}
Insert cell
H = µ(h)
Insert cell
// gridLines = ({} = {}) => ({aspect: {wide = 16, tall = 9} = {}, divisions = 3} = {}) => {
// return d3.range(aspect.wide * divisions + 1);
// }
Insert cell
gridLines()()
Insert cell
Insert cell
voteBoxes = ({size = 36} = {}) => (votes) =>
Plot.rect(
votes,
lighterBackground({
x1: d => d.y - size,
x2: d => d.y + size,
y1: d => d.x - size,
y2: d => d.x + size,
rx: size / 2,
fill: "grey",
stroke: "black",
strokeWidth: 3,
// fillOpacity: .5,
})
)
Insert cell
voteTexts = () => (votes) =>
Plot.text(
votes,
{
text: d => d.data.name,
...font,
fontWeight: 900,
fontVariant: "all-small-caps",
fontSize: 11,
x: "y",
y: "x",
}
)
Insert cell
Insert cell
categoryTexts = ({width}) => (categories) =>
Plot.text(
categories,
{
text: d => d.data.name,
...font,
fontWeight: 900,
fontSize: "larger",
x: d => d.y - d.side * p(width / 2),
y: "x",
}
)
Insert cell
categoryBoxes = ({size = p(30), aspect = 3 / 9} = {}) => (categories) =>
Plot.rect(
categories,
{
x1: d => (d.y - d.side * size),
x2: d => (d.y - 0 * size),
y1: d => d.x - size * aspect,
y2: d => d.x + size * aspect,
fill: "white",
// fillOpacity: .5,
rx: 9,
stroke: "category",
strokeWidth: 3,
}
)
Insert cell
Insert cell
facetFont = ({
...font,
// fontSize: 10,
fontWeight: 900,
})
Insert cell
facetTexts = ({side, margin = 2}) => (facets) =>
Plot.text(
facets.filter(k => k.side === side),
{
text: d => d.data.name,
textAnchor: anchor(side),
...facetFont,
x: d => d.children[0].y + p(side * margin),
y: d => d.x - dx / 2 * p(d.children.length + 1) - p(5),
}
)
Insert cell
facetBoxes = ({marginStart = 12} = {}) => (facets) =>
Plot.rect(
facets,
lighterBackground({
x1: d => d.children[0].y,
x2: d => d.children[0].y + d.side * (d.bbox.width + p(marginStart)),
y1: d => d.x - dx / 2 * p(d.children.length + 1) - p(10),
y2: d => d.x - dx / 2 * p(d.children.length + 1) + p(8),
fill: "category",
rx: 3,
stroke: "category",
strokeWidth: 3,
})
)
Insert cell
BBoxes = () => (facets) =>
Plot.rect(
facets,
{
x1: d => d.children[0].y,
// x2: d => d.children[0].y,
x2: d => d.children[0].y + d.side * d.bbox.width,
y1: d => d.x - dx / 2 * p(d.children.length + 1) - p(10),
y2: d => d.x - dx / 2 * p(d.children.length + 1) + p(10) + d.height,
fill: "pink",
// rx: 3,
// stroke: "category",
// strokeWidth: 3,
}
)
Insert cell
Insert cell
promptBoxes = () => (facets) =>
Plot.rect(
facets,
{
x1: d => d.children[0].y,
x2: d => d.children[0].y + (d.side * M(4.5)),
y1: facetBoxHalf(0)(-1),
y2: facetBoxHalf(0)(+1),
fill: "white",
// fill: "none",
rx: 3,
stroke: "category",
strokeWidth: 3,
}
)
Insert cell
promptTexts = ({side}) => (prompts) =>
Plot.text(
prompts.filter(k => k.side === side),
{
text: d => d.data.name,
textAnchor: anchor(side),

...font,

x: "y",
y: "x",
dx: µ(side),
}
)
Insert cell
facetBoxHalf =
(shift) =>
(topOrBottom) =>
({x, children}) =>
p(shift) + x + topOrBottom * dx / 2 * p(children.length + 1)
Insert cell
Insert cell
tracks = ({stroke, curve}) => (tracks) =>
Plot.link(
tracks,
{
x1: d => d.source.y,
y1: d => d.source.x,
x2: d => d.target.y,
y2: d => d.target.x,
stroke,
strokeWidth: d => 2 * d.source.height,
curve,
}
)
Insert cell
Insert cell
stations = (settings) => (stations) =>
Plot.dot(
stations,
{
x: "y",
y: "x",
stroke: "none", strokeWidth: d => (d.height + 1), fill: "white", stroke: "black",
r: d => d.height + 1,
}
)
Insert cell
Insert cell
// source: https://observablehq.com/@observablehq/plot-colorcontrast-custom-transform#colorContrast
lighterBackground =
options =>
Plot.initializer(
options,
(data, facets, channels, scales) => ({
channels: {
fill: {
...channels.fill,
scale: undefined,
value:
channels.fill == null // when options.fill is a constant color such as "red"
|| scales.color == null // when the scale isn't yet available (e.g. hexbin below)
? Array.from(data).fill(lighten(options.fill, k))
: Array.from(channels.fill.value, (value) => lighten(scales.color(value), k))
}
}
})
)
Insert cell
Insert cell
font = ({
fontFamily,
fontWeight: 500,
fontSize: 7,
})
Insert cell
Insert cell
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=${fontFamily}&display=swap">
Insert cell
fonts = [
"PT Sans Narrow",
"Roboto Condensed",
"Roboto",
"Courier",
]
Insert cell
getBBox = ({fontFamily, fontSize, fontWeight}) => (text) => {
const svg = d3.create('svg');
const tmp = svg
.append("text")
.style("font-family", fontFamily)
.style("font-size", fontSize)
.style("font-weight", fontWeight)
.text(text)
;
svg.node().appendChild(tmp.node());
document.body.appendChild(svg.node());
const bbox = tmp.node().getBBox();
document.body.removeChild(svg.node());
tmp.remove();
return {width: Math.ceil(bbox.width)};
}
Insert cell
Insert cell
uniques = (prop) =>
R.pipe(
R.map(R.prop(prop)),
R.uniq,
R.reject(R.isNil)
)
Insert cell
rotate = R.curry(
(n, list) =>
R.pipe(
R.splitAt(n % list.length),
R.reverse,
R.flatten,
)
(list)
)
Insert cell
addCategoryToTrack = (track) => ({...track, category: track.target.category}) // so tracks can be properly colored
Insert cell
isKind = (k) => ({kind}) => kind === k
Insert cell
isVote = isKind("vote")
Insert cell
isCategory = isKind("category")
Insert cell
isFacet = isKind("facet")
Insert cell
isPrompt = isKind("prompt")
Insert cell
but = (kind) => (link) => link.target.kind !== kind
Insert cell
linkIsKind = (kind) => ({target}) => target.kind === kind
Insert cell
linkIsVote = linkIsKind("vote")
Insert cell
anchor = (side) => ["end", "start"][(side + 1) / 2]
Insert cell
running = (list, sum = 0) => list.map(k => sum += k)
Insert cell
fullscreen = (cell) => {
const button = html`<button>Fullscreen`;
button.onclick = () => {
const {parentNode} = cell;
if (parentNode.requestFullscreen)
parentNode.requestFullscreen();
else if (parentNode.webkitRequestFullscreen)
parentNode.webkitRequestFullscreen();
};
return button;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
µ()/p()
Insert cell
Insert cell
Insert cell
Insert cell
contrast = (color) => lrgb_luminance(rgb_lrgb(d3.rgb(color))) >= 1/2 ? "black" : "white"
Insert cell
function lighten(color, k = 1) {
const {l, c, h} = d3.lch(color);
return d3.lch(k, c, h);
}
Insert cell
Insert cell
import {agileAdoption} from "@martien/agile-adoptie-argumentenkaart"
Insert cell
import {gemengdeScholen} from "@martien/argumentenkaart-gemengde-scholen"
Insert cell
import {analogVsDigital} from "@martien/analog-versus-digital-signal"
Insert cell
Insert cell
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