Public
Edited
Oct 20, 2023
Insert cell
Insert cell
chart = {
let w = 600;
let h = 600;
const svg = d3.create("svg")
.attr("viewBox", [0, 0, w, h]);
let mapping = [];
nodes.forEach((d,i) => {
mapping[+d['i']] = i;
});
let graph = {};
links.forEach(d => {
d['source'] = mapping[+d["i"]];
let key = +d["i"];
if(!(key in graph))
graph[key] = [];
let targets = graph[key];
d['target'] = mapping[+d["j"]];
targets.push(+d["j"]);
d['w'] = +d['w'];
// console.log(d)
})
nodes.forEach( n => {
let key = +n["i"];
if(graph[key])
n.degree = graph[key].length;
else
n.degree = 0;
})
let dataset = {
nodes: nodes,
edges: links
}
const degreeScale = d3.scaleLinear()
.domain(d3.extent(nodes.map(n => n.degree)))
.range([8,15]);
const hueScale = d3.scaleOrdinal(d3.schemeSet2)
.domain(d3.rollup(nodes, v => v.length, d => d["level"]).keys());
const edgeScale = d3.scaleLinear()
.domain([1,4])
.range([2,4])
//Create SVG element
// let svg = d3.select("body")
// .append("svg")
// .attr("width", w)
// .attr("height", h);
let svg2 = d3.select('body').append('svg')
.attr("width", w)
.attr("height", h)
.attr("id", "treeSVG")
drawTree(nodes[0]);
let simulation = d3.forceSimulation(dataset.nodes)
.force("charge", d3.forceManyBody())
.force("link", d3.forceLink(dataset.edges))
.force("center", d3.forceCenter().x(w/2).y(h/2))
.force("collision", d3.forceCollide(12));
const zoomRect = svg.append("rect")
.attr("width", w)
.attr("height", h)
.style("fill", "none")
.style("pointer-events", "all")
//Create edges as lines
let edgeMarks = svg.append("g");
edgeMarks.selectAll("line")
.data(dataset.edges)
.enter()
.append("line")
.style("stroke", "grey")
.style("stroke-width", e => {return edgeScale(e['w'])});
//Create nodes as circles
let nodeMarks = svg.append("g").attr("font-family", "sans-serif")
.attr("font-size", 8);
nodeMarks.selectAll("circle")
.data(dataset.nodes)
.enter()
.append("circle")
.attr("r", d => degreeScale(d.degree))
.attr('id', d => {
// console.log(d);
return 'n'+d.i;
})
.on("click", (event,d) => {
// console.log(d);
drawTree(d);
})
.style("stroke", "#000000")
.style("stroke-width", 1)
.style("fill", d => { return hueScale(d['level'])})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
.on("mouseover", function(event, d){
nodeMarks.selectAll("circle").style("opacity", 0.25).style("stroke-opacity", 0.25);
// console.log(d);
edgeMarks.selectAll("line")
.style("opacity", e => {
return (e.i == d.i) ? 1 : 0.1;
})
d3.select(this)
.style("stroke-width", 3)
.style("fill", "yellow")
.style("opacity", 1);
d3.select('#t'+d.i)
.style("stroke", "#000000")
.style("stroke-width", 3)
.attr("r", 5)
.style("fill", "yellow");
if(graph[d.i]){
graph[d.i].forEach( child => {
d3.select('#n'+child)
// .style("fill", "yellow")
.style("opacity", 1)
.style("stroke-opacity", 1);
})
}
})
.on("mouseout", function(event, d){
nodeMarks.selectAll("circle")
.style("opacity", 1)
.style("stroke-opacity", 1)
.style("fill", d => hueScale(d['level']));
edgeMarks.selectAll("line")
.style("opacity", 1)
d3.select(this)
.style("stroke-width", 1)
.style("fill", d => hueScale(d['level']))
d3.select('#t'+d.i)
.attr("r", 4)
// .style("stroke", "none")
.style("stroke-width", 1)
.style("fill", d => hueScale(d.data['level']))
// if(graph[d.i]){
// graph[d.i].forEach( child => {
// d3.select('#n'+child)
// .style("fill", d => hueScale(d['Ci']));
// })
// }
})
nodeMarks.selectAll("text")
.data(dataset.nodes)
.enter()
.append("text")
.attr("dy", "0.31em")
.attr("x", 0)
.attr("text-anchor", "middle")
.text(d => d.number);
const zoom = d3.zoom()
.scaleExtent([1/4, 10])
.on("zoom", zoomed);
let transform = d3.zoomIdentity;
zoomRect.call(zoom);
function zoomed(event) {
transform = event.transform;
nodeMarks.attr("transform", transform);
edgeMarks.attr("transform", transform);
}
//Add a simple tooltip
nodeMarks.selectAll("circle").append("title")
.text(d => {
return d.number + " " + d.name;
});
//Every time the simulation "ticks", this will be called
simulation.on("tick", () => {
// console.log(this);
edgeMarks.selectAll("line")
.attr("x1", d => { return d.source.x; })
.attr("y1", d => { return d.source.y; })
.attr("x2", d => { return d.target.x; })
.attr("y2", d => { return d.target.y; });
nodeMarks.selectAll("circle")
// .attr("transform", d => `translate(${d.y},${d.x})`);
.attr("cx", d => { return d.x; })
.attr("cy", d => { return d.y; });
nodeMarks.selectAll("text")
// .attr("transform", d => `translate(${d.y},${d.x})`);
.attr("x", d => { return d.x; })
.attr("y", d => { return d.y; });
});
function dragstarted(event, d) {
if (!event.active)
simulation.alphaTarget(0.1).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragended(event, d) {
if (!event.active)
simulation.alphaTarget(0);
d.fx = event.x;
d.fy = event.y;
}
function drawTree(rootNode) {
let copies = copyNodes(nodes);
let root = bfs(copies[mapping[+rootNode['i']]], copies);
root = tree(root);
// console.log(root);
function copyNodes(data){
let copiedNodes = [];
nodes.forEach((node,i) => {
copiedNodes[i] = {
name: node['dept'] + " " + node['number'],
i: +node['i'],
level: node['level'],
full: node['name']
};
});
return copiedNodes;
}
function bfs(d, nodes){
let visited = [];
let root = d;
let queue = [root];
do {
let curr = queue.shift();
curr.children = [];
visited.push(curr);
let children = graph[curr['i']];
if(children)
children.forEach( child => {
child = nodes[mapping[child]];
// if(!visited.includes(child) && !queue.includes(child)){
queue.push(child);
curr.children.push(child);
// }
});
} while (queue.length > 0);
sortLargest(root);
return d3.hierarchy(root);
}
function sortLargest(root){
// childMax = Math.max(childMax, root.children.length);
root.children.sort((a, b) => {
return b.children.length - a.children.length;
})
root.children.forEach(child => sortLargest(child));
}
function tree(root){
root.dx = 15;
root.dy = w / (root.height + 1);
return d3.cluster().nodeSize([root.dx, root.dy])(root);
}
let x0 = Infinity;
let x1 = -x0;
root.each(d => {
if (d.x > x1) x1 = d.x;
if (d.x < x0) x0 = d.x;
});
svg2.attr("viewBox", [-50, 0, w*1.2, x1 - x0 + root.dx * 2]);
svg2.selectAll("g").remove();
const g = svg2.append("g")
.attr("font-family", "sans-serif")
.attr("font-size", 14)
.attr("transform", `translate(${root.dy / 3},${root.dx - x0})`);
const link = g.append("g")
.attr("fill", "none")
.attr("stroke", "#555")
.attr("stroke-opacity", 0.4)
.attr("stroke-width", 1.5)
.selectAll("path")
.data(root.links())
.join("path")
.attr("d", d3.linkHorizontal()
.x(d => d.y)
.y(d => d.x));
const node = g.append("g")
.attr("stroke-linejoin", "round")
.attr("stroke-width", 3)
.selectAll("g")
.data(root.descendants())
.join("g")
.attr("transform", d => `translate(${d.y},${d.x})`);
node.append("circle")
.attr("fill", d => hueScale(d.data['level']))
.attr("r", 4)
.attr('id', d => {
// console.log(d);
return 't'+d.data.i;
})
.style("stroke", "#000000")
.style("stroke-width", 1)
.on("mouseover", function(event, d){
d3.select(this)
.style("fill", "yellow")
d3.select('#n'+d.data.i)
.style("fill", "yellow")
})
.on("mouseout", function(event, d){
d3.select(this)
.style("fill", d => hueScale(d.data['level']))
d3.select('#n'+d.data.i)
.style("fill", d => hueScale(d['level']));
});
node.append("text")
.attr("dy", "0.31em")
.attr("x", d => d.children ? -6 : 6)
.attr("text-anchor", d => d.children ? "end" : "start")
.text(d => d.data.name)
.clone(true).lower()
.attr("stroke", "white");
node.selectAll("circle").append("title")
.text(d => {
console.log(d);
return d.data.full;
});
}
}
Insert cell
nodes = d3.csvParse(await FileAttachment("phys_data.csv").text(), d3.autoType)
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