Public
Edited
Mar 26, 2023
Insert cell
md`# Week 8 Network Data Visualization`
Insert cell
data = FileAttachment("miserables.json").json()
Insert cell
height = width * .6
Insert cell
md`## Node Link Diagram`
Insert cell
nl_diagram_init = {
const links = data.links.map(d => Object.create(d));
const nodes = data.nodes.map(d => Object.create(d));
const color = d3.scaleOrdinal().domain(nodes.map(d=>d.group).sort(d3.ascending)).range(d3.schemeCategory10);

const simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links).id(d => d.id))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));

const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height]);

const link = svg.append("g")
.attr("stroke", "#999")
.attr("stroke-opacity", 0.6)
.selectAll("line")
.data(links)
.join("line")
.attr("stroke-width", d => Math.sqrt(d.value));

const node = svg.append("g")
.attr("stroke", "#fff")
.attr("stroke-width", 1.5)
.selectAll("circle")
.data(nodes)
.join("circle")
.attr("r", 5)
.attr("fill", color)
.call(drag(simulation));

node.append("title")
.text(d => d.id);

simulation.on("tick", () => {
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);

node
.attr("cx", d => d.x)
.attr("cy", d => d.y);
});

invalidation.then(() => simulation.stop());

return svg.node();
}
Insert cell
Insert cell
nl_diagram = {
const links = data.links.map(d => Object.create(d));
const nodes = data.nodes.map(d => Object.create(d));
const color = d3.scaleOrdinal().domain(nodes.map(d => d.group).sort(d3.ascending)).range(d3.schemeCategory10);

const simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links).id(d => d.id))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));

const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height]);

const link = svg.append("g")
.attr("stroke", "#999")
.attr("stroke-opacity", 0.6)
.selectAll("line")
.data(links)
.join("line")
.attr("stroke-width", d => Math.sqrt(d.value));

const node = svg.append("g")
.attr("stroke", "#fff")
.attr("stroke-width", 1.5)
.selectAll("circle")
.data(nodes)
.join("circle")
.attr("r", 5)
.attr("fill", color)
.call(drag(simulation));

node.append("title")
.text(d => d.id);

simulation.on("tick", () => {
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);

node
.attr("cx", d => d.x)
.attr("cy", d => d.y);
});

const select = d3.select("#select-node")
.append("select")
.on("change", onSelectNode);

select.selectAll("option")
.data(nodes)
.join("option")
.attr("value", d => d.id)
.text(d => d.id);

invalidation.then(() => simulation.stop());

function onSelectNode() {
console.log("select node");
const selectedNodeId = select.property("value");
console.log("selectedNodeId:", selectedNodeId);
const selectedNode = nodes.find(d => d.id === selectedNodeId);
console.log("selectedNode:", selectedNode);
if (selectedNode) {
// Set the x and y coordinates of the selected node to the center of the SVG
selectedNode.fx = width / 2;
}
}
return svg.node();
};
Insert cell
drag = simulation => {
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}
return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
Insert cell
md`## Adjacency Matrix`
Insert cell
adj_matrix = {
const links = data.links.map(d => Object.create(d));
const nodes = data.nodes.map(d => Object.create(d));
var adj_list = [];
nodes.forEach(function(source_node, source_index){
nodes.forEach(function(target_node, target_index){
var temp = links.filter(d=>(d.source == source_node.id) && (d.target==target_node.id));
if (temp.length > 0){
if(adj_list.filter(d=>(d.source == source_node.id) && (d.target==target_node.id)).length > 0){
var s = temp[0].target;
var t = temp[0].source;
var v = temp[0].value;
adj_list.push({'source': s, 'target': t, 'value': v});
}else{
adj_list.push(temp[0]);
}
}else{
if(adj_list.filter(d=>(d.source == source_node.id) && (d.target==target_node.id)).length > 0){
var s = target_node.id;
var t = source_node.id;
var v = 0;
adj_list.push({'source': s, 'target': t, 'value': v});
}else{
adj_list.push({'source': source_node.id, 'target': target_node.id, 'value': 0});
}
}
})
})
const color = d3.scaleSymlog().domain([0, d3.max(adj_list, d=>d.value)]).range([d3.interpolateOranges(0), d3.interpolateOranges(1)]);
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, width]);

const scale = d3.scalePoint().domain(nodes.map(d=>d.id).sort(d3.ascending)).range([0, width]).padding(1);

svg.selectAll('rect')
.data(adj_list)
.enter()
.append('rect')
.attr('width', d => scale.step())
.attr('height', d => scale.step())
.attr('x', d => scale(d.source))
.attr('y', d => scale(d.target))
.style('stroke', 'none')
.style('stroke-width', '1px')
.style('fill', d => color(d.value))
.append("title")
.text(d => d.source + "-" + d.target)
return svg.node();
}
Insert cell
md`## Chord Diagram`
Insert cell
chord_diagram = {
const links = data.links.map(d => Object.create(d));
const nodes = data.nodes.map(d => Object.create(d));
const groups = [...new Set(nodes.map(d=>d.group).sort(d3.ascending))];
var chord_links = [];
groups.forEach(function(source_node, source_index){
chord_links.push([]);
groups.forEach(function(target_node, target_index){
var sn = nodes.filter(d=>d.group == source_node).map(d=>d.id);
var tn = nodes.filter(d=>d.group == target_node).map(d=>d.id);
var value1 = d3.sum(links.filter(d=> (sn.indexOf(d.source)>=0) && (tn.indexOf(d.target)>=0)), d=>d.value);
var value2 = d3.sum(links.filter(d=> (sn.indexOf(d.target)>=0) && (tn.indexOf(d.source)>=0)), d=>d.value)
chord_links[source_index].push(value1+value2);
})
})
const svg = d3.create("svg")
.attr("viewBox", [-width / 2, -height / 2, width, height]);
const innerRadius = Math.min(width, height) * 0.5 - 20;
const outerRadius = innerRadius + 6
const arc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
const ribbon = d3.ribbonArrow()
.radius(innerRadius - 0.5)
.padAngle(1 / innerRadius);
const color = d3.scaleOrdinal().domain(nodes.map(d=>d.group).sort(d3.ascending)).range(d3.schemeCategory10);
const chord = d3.chordDirected()
.padAngle(12 / innerRadius)
.sortSubgroups(d3.descending)
.sortChords(d3.descending);

const chords = chord(chord_links);

const textId = DOM.uid("text");

svg.append("path")
.attr("id", textId.id)
.attr("fill", "none")
.attr("d", d3.arc()({outerRadius, startAngle: 0, endAngle: 2 * Math.PI}));

svg.append("g")
.attr("fill-opacity", 0.75)
.selectAll("g")
.data(chords)
.join("path")
.attr("d", ribbon)
.attr("fill", d => color(groups[d.target.index]))
.style("mix-blend-mode", "multiply")

svg.append("g")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.selectAll("g")
.data(chords.groups)
.join("g")
.call(g => g.append("path")
.attr("d", arc)
.attr("fill", d => color(groups[d.index]))
.attr("stroke", "#fff"))
.call(g => g.append("text")
.attr("dy", -3)
.append("textPath")
.attr("xlink:href", textId.href)
.attr("startOffset", d => d.startAngle * outerRadius)
.text(d => groups[d.index]))
return svg.node();
}
Insert cell
md`## Edge Bundling`
Insert cell
eb_chart = {
const radius = width / 2
const line = d3.lineRadial()
.curve(d3.curveBundle.beta(.75))
.radius(d => d.y)
.angle(d => d.x);
const tree = d3.cluster()
.size([2 * Math.PI, radius - 100]);
const root = tree(bilink(d3.hierarchy(graph)
.sort((a, b) => d3.ascending(a.height, b.height) || d3.ascending(a.data.id, b.data.id))));

const svg = d3.create("svg")
.attr("viewBox", [-width / 2, -width / 2, width, width]);

const node = svg.append("g")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.selectAll("g")
.data(root.leaves())
.join("g")
.attr("transform", d => `rotate(${d.x * 180 / Math.PI - 90}) translate(${d.y},0)`)
.append("text")
.attr("dy", "0.31em")
.attr("x", d => d.x < Math.PI ? 6 : -6)
.attr("text-anchor", d => d.x < Math.PI ? "start" : "end")
.attr("transform", d => d.x >= Math.PI ? "rotate(180)" : null)
.text(d => d.data.id)
.each(function(d) { d.text = this; })
.call(text => text.append("title").text(d => `${d.data.id}
${d.outgoing.length} outgoing
${d.incoming.length} incoming`));

const link = svg.append("g")
.attr("stroke", "black")
.attr("fill", "none")
.selectAll("path")
.data(root.leaves().flatMap(leaf => leaf.outgoing))
.join("path")
.style("mix-blend-mode", "multiply")
.attr("d", ([i, o]) => line(i.path(o)))
.each(function(d) { d.path = this; });

return svg.node();
}
Insert cell
graph = {
const {nodes, links} = data;
const groupById = new Map;
const nodeById = new Map(nodes.map(node => [node.id, node]));

for (const node of nodes) {
let group = groupById.get(node.group);
if (!group) groupById.set(node.group, group = {id: node.group, children: []});
group.children.push(node);
node.targets = [];
}

for (const {source: sourceId, target: targetId} of links) {
nodeById.get(sourceId).targets.push(targetId);
}

return {children: [...groupById.values()]};
}
Insert cell
Insert cell
md `# Arc Diagram`
Insert cell
arc_diagram = {
const svg = d3.select(DOM.svg(width, width));

svg.append("style").text(`

.hover path {
stroke: #ccc;
}

.hover text {
fill: #ccc;
}

.hover g.primary text {
fill: black;
font-weight: bold;
}

.hover g.secondary text {
fill: #333;
}

.hover path.primary {
stroke: #333;
stroke-opacity: 1;
}

`);
const margin = ({top: 20, right: 20, bottom: 20, left: 100});
const step = 14;
const color = d3.scaleOrdinal(arc_graph.nodes.map(d => d.group).sort(d3.ascending), d3.schemeCategory10);
const y = d3.scalePoint(arc_graph.nodes.map(d => d.id).sort(d3.ascending), [margin.top, width - margin.bottom]);
const arc = function (d) {
const y1 = d.source.y;
const y2 = d.target.y;
const r = Math.abs(y2 - y1) / 2;
return `M${margin.left},${y1}A${r},${r} 0,0,${y1 < y2 ? 1 : 0} ${margin.left},${y2}`;
}

const label = svg.append("g")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.attr("text-anchor", "end")
.selectAll("g")
.data(arc_graph.nodes)
.join("g")
.attr("transform", d => `translate(${margin.left},${d.y = y(d.id)})`)
.call(g => g.append("text")
.attr("x", -6)
.attr("dy", "0.35em")
.attr("fill", d => d3.lab(color(d.group)).darker(2))
.text(d => d.id))
.call(g => g.append("circle")
.attr("r", 3)
.attr("fill", d => color(d.group)));

const path = svg.insert("g", "*")
.attr("fill", "none")
.attr("stroke-opacity", 0.6)
.attr("stroke-width", 1.5)
.selectAll("path")
.data(arc_graph.links)
.join("path")
.attr("stroke", d => d.source.group === d.target.group ? color(d.source.group) : "#aaa")
.attr("d", arc);

const overlay = svg.append("g")
.attr("fill", "none")
.attr("pointer-events", "all")
.selectAll("rect")
.data(arc_graph.nodes)
.join("rect")
.attr("width", margin.left + 40)
.attr("height", step)
.attr("y", d => y(d.id) - step / 2)
.on("mouseover", d => {
svg.classed("hover", true);
label.classed("primary", n => n === d);
label.classed("secondary", n => n.sourceLinks.some(l => l.target === d) || n.targetLinks.some(l => l.source === d));
path.classed("primary", l => l.source === d || l.target === d).filter(".primary").raise();
})
.on("mouseout", d => {
svg.classed("hover", false);
label.classed("primary", false);
label.classed("secondary", false);
path.classed("primary", false).order();
});
const order = (a, b) => d3.sum(b.sourceLinks, l => l.value) + d3.sum(b.targetLinks, l => l.value) - d3.sum(a.sourceLinks, l => l.value) - d3.sum(a.targetLinks, l => l.value) || d3.ascending(a.id, b.id);

function update() {
y.domain(arc_graph.nodes.sort(d3.ascending(order)).map(d => d.id));

const t = svg.transition()
.duration(750);

label.transition(t)
.delay((d, i) => i * 20)
.attrTween("transform", d => {
const i = d3.interpolateNumber(d.y, y(d.id));
return t => `translate(${margin.left},${d.y = i(t)})`;
});

path.transition(t)
.duration(750 + arc_graph.nodes.length * 20)
.attrTween("d", d => () => arc(d));

overlay.transition(t)
.delay((d, i) => i * 20)
.attr("y", d => y(d.id) - step / 2);
}

return svg.node();
}
Insert cell
arc_graph = {
const nodes = data.nodes.map(({id, group}) => ({
id,
sourceLinks: [],
targetLinks: [],
group
}));

const nodeById = new Map(nodes.map(d => [d.id, d]));

const links = data.links.map(({source, target, value}) => ({
source: nodeById.get(source),
target: nodeById.get(target),
value
}));

for (const link of links) {
const {source, target, value} = link;
source.sourceLinks.push(link);
target.targetLinks.push(link);
}

return {nodes, links};
}
Insert cell
d3 = require("d3@7")
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