Insert cell
md`# Debtors, network with zoom, connected nodes on click and tooltip

Based on JR Ladd's [Marvel network example](https://observablehq.com/@jrladd/marvel-network)`
Insert cell
chart = {
const links = json.links.map(d => Object.create(d));
const nodes = json.nodes.map(d => Object.create(d));
const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);

svg
.append('rect')
.attr('width', '100%')
.attr('height', '100%')
.attr('fill', 'white')
.on('click', function() {
d3.selectAll('.link').style('opacity', '1');
d3.selectAll('.node').style('opacity', '1');
});

var container = svg.append('g');

svg.call(zoom(container));

var link = container
.append("g")
.attr("class", "links")
.selectAll("line");

var node = container
.append("g")
.attr("class", "nodes")
.selectAll("circle");

// Make object of all neighboring nodes.
let toggle = 0;
var linkedByIndex = {};

// keep the clicked node in linkedByIndex
for (let i = 0; i < nodes.length; i++) {
linkedByIndex[i + "," + i] = 1;
}

// Get the neighboring nodes and put them in linkedByIndex
json.links.forEach(function(d) {
linkedByIndex[d.source.index + ',' + d.target.index] = 1;
linkedByIndex[d.target.index + ',' + d.source.index] = 1;
});

// A function to test if two nodes are neighboring.
function neighboring(a, b) {
return linkedByIndex[a.index + ',' + b.index];
}

function connectedNodes() {
if (toggle == 0) {
let d = d3.select(this).node().__data__;
var neighbors = [];
node.style("opacity", function(o) {
neighboring(d, o) || neighboring(o, d) ? neighbors.push(o.id) : null;
return neighboring(d, o) || neighboring(o, d) ? 1 : 0.15;
});
link.style("opacity", function(o) {
return o.target == d || o.source == d ? 1 : 0.15;
});
var contents = [];
neighbors.forEach(function(item) {
contents.push("<li>" + item + "</li>");
});
console.log(contents);
return tooltip.html(
// style the contents of your tooltip here. This will replace the hover tooltip contents in tooltip below
"<h4>" + d.id + "</h4><ul>" + contents.join("") + "</ul>"
);

toggle = 1;
} else {
link.style("opacity", 1);
node.style("opacity", 1);
toggle = 0;
}
}

return Object.assign(svg.node(), {
update(nodes, links) {
link = link.data(links, function(d) {
return d.source.index + ", " + d.target.index;
});

link.exit().remove();

var linkEnter = link
.enter()
.append("line")
.attr('class', 'link')
.attr('stroke', 'gray');

link = linkEnter.merge(link);

node = node.data(nodes);

node.exit().remove();

var nodeEnter = node
.enter()
.append("circle")
.attr('r', function(d, i) {
return degreeSize(d.betweenness);
})
.attr("fill", color)
.attr('class', 'node')
.on("mouseover", (event, d) => tooltip_in(event, d))
.on("mouseout", tooltip_out)
.on('click', connectedNodes)
.call(drag(simulation));

node = nodeEnter.merge(node);

simulation.nodes(nodes).on("tick", ticked);

simulation.force("link").links(links);

simulation.alpha(1).restart();

function ticked() {
link
.attr("x1", function(d) {
return d.source.x;
})
.attr("y1", function(d) {
return d.source.y;
})
.attr("x2", function(d) {
return d.target.x;
})
.attr("y2", function(d) {
return d.target.y;
});

node
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
}
}
});
}
Insert cell
md`## Define tooltip`
Insert cell
tooltip = d3
.select("body")
.append("div")
.style("position", "absolute")
.style("visibility", "hidden")
.style("background-color", "white")
.attr("class", "tooltip")
Insert cell
tooltip_in = function(event, d) {
return tooltip
.html("<h4>" + d.id + "</h4>")
.style("visibility", "visible")
.style("top", event.pageY + "px")
.style("left", event.pageX + "px");
}
Insert cell
tooltip_out = function() {
return tooltip
.transition()
.duration(50)
.style("visibility", "hidden");
}
Insert cell

html`
<style type="text/css">
.tooltip {
fill: white;
font-family: sans-serif;
padding: .5rem;
border-radius: 8px;
border: 1px solid grey;
}

</style>
`
Insert cell
md`## Size, color, and height utilities`
Insert cell
degreeSize = d3
.scaleLinear()
.domain([
d3.min(json.nodes, function(d) {
return d.betweenness;
}),
d3.max(json.nodes, function(d) {
return d.betweenness;
})
])
.range([5, 25])
Insert cell
color = {
const scale = d3.scaleOrdinal(d3.quantize(d3.interpolateRainbow, 12));
return d => scale(d.modularity);
}
Insert cell
height = 900
Insert cell
md`## Define drag, zoom, and network forces`
Insert cell
drag = simulation => {
function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).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 = null;
d.fy = null;
}
return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
Insert cell
zoom = container => {
function zoomed(event, d) {
container.attr("transform", "translate(" + event.transform.x + ", " + event.transform.y + ") scale(" + event.transform.k + ")");
}
return d3.zoom().on('zoom', zoomed)
}
Insert cell
simulation = d3
.forceSimulation()
.force(
"link",
d3.forceLink().id(function(d) {
return d.id;
})
)
.force(
"charge",
d3
.forceManyBody()
.strength([-250])
.distanceMax([500])
)
.force("center", d3.forceCenter(width / 2, height / 2))
.force(
"collide",
d3.forceCollide().radius(function(d) {
return degreeSize(d.betweenness);
})
)
Insert cell
md`## Get the data and draw the chart`
Insert cell
json = FileAttachment("tatum_filtered.json").json()
Insert cell
chart.update(json.nodes,json.links)
Insert cell
d3 = require("d3@6")
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