Published
Edited
Apr 26, 2021
1 fork
2 stars
Insert cell
Insert cell
// adapted from https://www.d3-graph-gallery.com/graph/arc_highlight.html
interactiveNodesAndArcs = {
const radius = 8
const container = d3.create("svg")
.attr("viewBox", [0, 0, width+margin.left+margin.right, height+margin.top+margin.bottom]);
const arcGroup = container
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
// This code builds up the SVG path element; see nodesAndArcs for details
function buildArc(d) {
let start = yScale(idToNode[d.source].name);
let end = yScale(idToNode[d.target].name);
const arcPath = ['M', 220, start, 'A', (start - end)/8, ',', (start-end)/4.2, 0,0,",",
start < end ? 1: 0, 220, end].join(' ');
return arcPath;
}
// create the arcs
const arcs = arcGroup.selectAll("arcs")
.data(graphData.links)
.enter().append("path")
.attr("d", d => buildArc(d))
.style("fill", "none")
.attr("stroke", "#ffb9bb")
.style('stroke-width', 1)
// create the nodes
const nodes = arcGroup.selectAll("nodes")
.data(graphData.nodes)
.enter().append("circle")
.attr("cx", 220)
.attr("cy", d => yScale(d.name))
.attr("r", radius)
.attr("fill", "#ffb9bb")
.attr("id", d => d.id)
// create the node labels
const labels = arcGroup.selectAll("nodeLabels")
.data(graphData.nodes)
.enter().append("text")
.attr("x", 200)
.attr("y", d => yScale(d.name) + 5)
.attr("fill", function (d) {
return d.group == 1 ? '#ff5357' : 'darkgrey';})
.style("text-anchor", "end")
.style("font-size", function (d) {
return d.group == 1 ? '14px' : '12px';})
.text(d => d.name)
const info = arcGroup.selectAll("nodeInfo")
.data(graphData.nodes)
.enter()
.append("text")
.attr("x", 240)
.attr("y", d => yScale(d.name) + 5)
.style("text-anchor", "left")
.style("font-size", "12px")
.text(d => d.description)
.attr('visibility', 'hidden')
// When the user mouses over a node,
// add interactive highlighting to see connections between nodes
nodes.on('mouseover', function(d) {
let thisNode = this;
// highlight only the selected node
d3.select(this)
.style("fill", "peachpuff");
info.filter(function(noded) { return noded.id == thisNode.id })
.attr('visibility', 'visible')
nodes
.filter(function(noded) {
return idToTargetNodes[thisNode.id].includes(noded.id) || idToSourceNodes[thisNode.id].includes(noded.id)})
.style("fill", "peachpuff");
nodes
.filter(function(noded) {
return !(idToTargetNodes[thisNode.id].includes(noded.id)) && !(idToSourceNodes[thisNode.id].includes(noded.id)) && !(thisNode.id == noded.id)})
.style('opacity', '0.3')
// next, style the arcs
arcs
// the arc color and thickness stays as the default unless connected to the selected node d
// notice how embedding the reference to arcs within nodes.on() allows the code to connect d to arcd
// this code iterates through all the arcs so we can compare each to the selected node d
.style('stroke', function (arcd) {
return arcd.source === d.id || arcd.target === d.id ? 'peachpuff' : '#ffb9bb';})
.style('stroke-width', function (arcd) {
return arcd.source === d.id || arcd.target === d.id ? 4 : 1;})
.style('opacity', function (arcd) {
return arcd.source === d.id || arcd.target === d.id ? 1 : 0.3;})
// next, color and bold the text
labels
.style("font-weight", function (labeld) {
return labeld.id === d.id ? "bold" : "normal";})
.style("opacity", function (labeld) {
return labeld.id === d.id || idToTargetNodes[thisNode.id].includes(labeld.id) || idToSourceNodes[thisNode.id].includes(labeld.id) ? 1 : 0.3;})
labels
.filter(function (labeld) {
return idToTargetNodes[thisNode.id].includes(labeld.id) || idToSourceNodes[thisNode.id].includes(labeld.id)
})
.style("font-weight", 'bold')
});
// remove highlighting when user mouse moves out of node by restoring default colors and thickness
nodes.on('mouseout', function (d) {
info.attr('visibility', 'hidden')
nodes
.style("fill", "#ffb9bb")
.style('opacity', 1);
arcs
.style('stroke', '#ffb9bb')
.style('stroke-width', 1)
.style('opacity', 1);
labels
.style('font-weight', 'normal')
.style('fill', function (d) {
return d.group == 1 ? '#ff5357' : 'darkgrey';})
.style('opacity', 1);
});
return container.node();
}
Insert cell
graphData = FileAttachment("food_network@6.json").json()
Insert cell
Insert cell
d3 = require("d3@^5.8")
Insert cell
idToNode =
{
let dict = {};
graphData.nodes.forEach(function(n) {
dict[n.id] = n;
});
return dict;
}
Insert cell
margin = ({top: 20, bottom: 20, left: 30, right: 30});
Insert cell
width = 950 - margin.left - margin.right;
Insert cell
height = 2000 - margin.top - margin.bottom;
Insert cell
xScale = d3.scalePoint()
.domain(allNodesNames)
.range([0,width])
Insert cell
idToTargetNodes =
{
let dict = {};
graphData.nodes.forEach(function (n) {
dict[n.id] = [];
graphData.links.forEach(function (l) {
if (l.source === n.id) {
dict[n.id].push(l.target);
}
});
});
return dict;
}
Insert cell
idToSourceNodes =
{
let dict = {};
graphData.nodes.forEach(function (n) {
dict[n.id] = [];
graphData.links.forEach(function (l) {
if (l.target === n.id) {
dict[n.id].push(l.source);
}
});
});
return dict;
}
Insert cell
yScale = d3.scalePoint()
.domain(allNodesNames)
.range([0,height])
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