Published
Edited
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
md`<figure>
<img src="${await FileAttachment("titanicTree.jpg").url()}">
</figure>`
Insert cell
Insert cell
category_array=["Sex", "Survived", "Age", "Class", "value"]
Insert cell
//Draw the tree here using the graphTree function. It could look simlar to that.
//graphTree(hierarchyData, {label: d => d.data.name})

graphTree(d3.hierarchy(nestedData[0], function(d) { return d.values; }), {label: d => d.data.key})

/*
label: d => `height ${d.height}`
label: d => `depth ${d.depth}`
*/
Insert cell
// Add the depth and height information to the nested data
// https://stackoverflow.com/questions/12414837/d3-nesting-on-several-keys-with-a-loop

nestedData = {

var nest = d3.nest()
.rollup(function(leaves) { return leaves.length; })
.key(function(d) { return "root"; });
category_array.forEach(function(key) {
nest.key(function(d) {return d[key]; })
});
nest = nest.entries(data);
return nest;
}

/*
// alternative
let nest = d3.nest()
.key(function(d) { return "root"; })
.key(function(d) { return d[category_array[0]]; })
.key(function(d) { return d[category_array[1]]; })
.key(function(d) { return d[category_array[2]]; })
.key(function(d) { return d[category_array[3]]; })
.key(function(d) { return d[category_array[4]]; })
.rollup(function(leaves) { return leaves.length; })
.entries(data);
*/
Insert cell
Insert cell
Insert cell
Insert cell
parallelSet = {
const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);
var color = d3.scaleOrdinal(d3.schemePastel2);
// Constructs a new Sankey generator
const sankey = d3.sankey()
.size([width, height - 10])
.nodeWidth(5) // Sets the node width to the specified number
.nodePadding(20) // Vertical separation between nodes at each col. to the specified number
.nodeSort(null/*[sort]*/) // If sort is null, the order is fixed by the input
.linkSort(null);
/* The order of the categories in the axes needs to be linked with the category_array
but the values don't need an axis because they are shown individually for each
attribute (-relation via mouse) */
let key_array = [];
let key_index = -1;
for (let i = 0; i < category_array.length; i++) {
if(category_array[i] == "value"){
console.log("Found value and cutting it off for visualization - " + i + ". position!");
continue;
}else{
key_index++;
}
key_array[key_index] = category_array[i];
}
// copy nodes and links of d3 sankey
const {nodes, links} = sankey(graph(key_array));

var node = svg.append("g")
.selectAll("rect")
.data(nodes).enter()
.append("rect")
.attr("x", d => d.x0)
.attr("y", d => d.y0)
.attr("height", d => d.y1 - d.y0)
.attr("width", d => d.x1 - d.x0); // sankey.nodeWidth()
var link = svg.append("g")
.attr("fill", "none")
.selectAll("g")
.data(links).enter()
.append("path")
.attr("d", d3.sankeyLinkHorizontal())
.attr("stroke", d => color(d.names[0])) // color of first attribute (left to right)
.style("stroke-width", d => Math.max(1, d.width))
// specifies how an element’s content should blend with its background
.style("mix-blend-mode", "multiply"/*"overlay"*/)
// shows specific relation value in mouse-window
.append("title")
.text(d => d.names.join(" -> ")+ ": " + d.value)
// relationship of 2 attribute axis
// .text(d => d.source.name + " -> " + d.target.name + ": " + d.value);
var node_text = svg.append("g")
// .style("font", "12px sans-serif")
.attr("font-family", "sans-serif")
.attr("font-size", 12)
.selectAll("text")
.data(nodes).enter()
.append("text")
.attr("x", d => d.x0 < width / 2 ? d.x1 + 6 : d.x0 - 6)
.attr("y", d => (d.y1 + d.y0) / 2)
.attr("text-anchor", "end")
.text(d => d.name + ": " + d.value)
.filter(d => d.x0 < width / 2)
.attr("text-anchor", "start");

return svg.node();
}

/*
sources:
https://observablehq.com/@d3/parallel-sets?collection=@d3/d3-sankey
https://jarrettmeyer.com/2018/05/31/creating-a-d3-sankey-graph
https://observablehq.com/@aaronkyle/sankey-diagram-d3v5
*/
Insert cell

// source: https://observablehq.com/@d3/parallel-sets?collection=@d3/d3-sankey

// d3 sankey needs a specific graph structure with nodes and links (source-target-relations)
function graph(key_array) {
let index = -1;
const nodes = [];
const nodeByKey = new Map;
const indexByKey = new Map;
const links = [];
// key structure follows the order of category array
for (const k of key_array) {
for (const d of data) {
const key = JSON.stringify([k, d[k]]); // convert js-value to JSON string k = attribute
if (nodeByKey.has(key)) continue;
// if(key == "value") continue; // doens't show final value
const node = {name: d[k]};
nodes.push(node);
nodeByKey.set(key, node);
indexByKey.set(key, ++index);
}
}

for (let i = 1; i < key_array.length; ++i) {
const a = key_array[i - 1];
const b = key_array[i];
const prefix = key_array.slice(0, i + 1);
const linkByKey = new Map;
for (const d of data) {
const names = prefix.map(k => d[k]); // get names of a relation step
const key = JSON.stringify(names);
const value = d.value || 1; // get value of data
let link = linkByKey.get(key);
if (link) { link.value += value; continue; } // calculate combined value of current attribute type
link = {
source: indexByKey.get(JSON.stringify([a, d[a]])),
target: indexByKey.get(JSON.stringify([b, d[b]])),
names,
value
};
links.push(link);
linkByKey.set(key, link);
}
}

return {nodes, links};
}
Insert cell
md`## Apendix`
Insert cell
data = d3.csvParse(await FileAttachment("titanic.csv").text(), d3.autoType)
Insert cell
import { table } from "@tmcw/tables/2"
Insert cell
dx = 12
Insert cell
dy = 90
Insert cell
Insert cell
tree = d3.tree().nodeSize([dx, dy])
Insert cell
shape2path = require("shape2path@3")
Insert cell
d3 = require("d3@5", "d3-sankey@0.12")
Insert cell
height = 400
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