study-buddy / app /agents /d3 /templates /network_force_graph.py
GitHub Actions
deploy d092bea3608b7a29952f16357fda39b7a29e399b
2e818da
Raw
History Blame Contribute Delete
4.32 kB
"""Force-directed network graph template — network family.
Adapted from d3-graph-gallery.com (Yan Holtz) —
https://d3-graph-gallery.com/graph/network_basic.html (d3 v6 version:
d3.forceSimulation() with forceLink/forceManyBody/forceCenter, driven by
data_network.json loaded via d3.json()). Here the JSON is injected inline
as GraphData; nodes are additionally colored by `group` via an ordinal
color scale, ticking uses the "tick" event (not "end") so the simulation
animates into place, and a drag behavior is added so nodes can be
repositioned.
"""
from __future__ import annotations
from app.agents.d3.registry import D3Template, register
from app.agents.d3.templates._graph import GraphData, GraphNode, GraphLink
_HTML = """<!-- Adapted from d3-graph-gallery.com (Yan Holtz) — https://d3-graph-gallery.com/graph/network_basic.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
html, body { margin: 0; padding: 0; width: 100vw; height: 100vh; background: #FAF7F2; color: #1A3557; font-family: Georgia, 'Libre Caslon Text', serif; overflow: hidden; }
#my_dataviz { width: 100vw; height: 100vh; }
.chart-title { fill: #1A3557; font-size: 18px; text-anchor: middle; }
.link { stroke: #1A3557; stroke-opacity: 0.35; }
.node { stroke: #1A3557; stroke-width: 1.5px; cursor: grab; }
</style>
</head>
<body>
<div id="my_dataviz"></div>
<script>
const data = __DATA__;
const margin = {top: 50, right: 30, bottom: 20, left: 30},
width = 960 - margin.left - margin.right,
height = 600 - margin.top - margin.bottom;
const svg = d3.select("#my_dataviz")
.append("svg")
.attr("viewBox", `0 0 ${width + margin.left + margin.right} ${height + margin.top + margin.bottom}`)
.attr("preserveAspectRatio", "xMidYMid meet")
.style("width", "100%")
.style("height", "100%")
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
svg.append("text")
.attr("class", "chart-title")
.attr("x", width / 2)
.attr("y", -20)
.text(data.title);
const groups = Array.from(new Set(data.nodes.map(d => d.group)));
const color = d3.scaleOrdinal(groups, ["#4A7FB5", "#B5764A", "#6FA37A", "#A05A9C", "#C2A03C"]);
function 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);
}
const simulation = d3.forceSimulation(data.nodes)
.force("link", d3.forceLink(data.links).id(d => d.id))
.force("charge", d3.forceManyBody().strength(-250))
.force("center", d3.forceCenter(width / 2, height / 2))
.on("tick", ticked);
const link = svg
.selectAll("line")
.data(data.links)
.join("line")
.attr("class", "link")
.style("stroke-width", d => Math.sqrt(d.value));
const node = svg
.selectAll("circle")
.data(data.nodes)
.join("circle")
.attr("class", "node")
.attr("r", 10)
.style("fill", d => color(d.group))
.call(drag(simulation));
function ticked() {
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);
}
</script>
</body>
</html>
"""
golden_sample = GraphData(
title="Demo",
nodes=[
GraphNode(id="A", group="x"),
GraphNode(id="B", group="x"),
GraphNode(id="C", group="y"),
],
links=[
GraphLink(source="A", target="B", value=2),
GraphLink(source="B", target="C", value=1),
],
)
register(
D3Template(
id="network_force_graph",
family="network",
title="Force-directed network graph",
when_to_use="Show relationships between named entities as a connected node-link network.",
data_requirements="A list of nodes (id, optional group) and links (source id, target id, optional weight).",
schema=GraphData,
html_template=_HTML,
golden_sample=golden_sample,
)
)