import math
import networkx as nx
from js import d3
from pyodide.ffi import create_proxy, to_js
G = nx.karate_club_graph()
node_list = [{"id": node[0], "name": node[1]} for node in G.nodes(data="club")]
edge_list = [{"source": edge[0], "target": edge[1]} for edge in nx.to_edgelist(G)]
avg_conn = nx.average_node_connectivity(G)
graph_radius = nx.radius(G)
center_nodes = nx.center(G)
stat = Element("stat")
stat.element.innerText = f"""Avg. node connectivity is {avg_conn}
Radius of the graph is {graph_radius}
Center nodes are: {center_nodes}"""
margin = {"top": 30, "right": 30, "bottom": 70, "left": 40}
width = 1000 - margin["left"] - margin["right"]
max_height = 800 - margin["top"] - margin["bottom"]
viz = d3.select("#viz")
viz.select(".loading").remove()
svg = (viz
.append("svg")
.attr("width", width + margin["left"] + margin["right"])
.attr("height", max_height + margin["top"] + margin["bottom"])
.append("g")
.attr("transform",
f"translate({margin['left']},{margin['top']})")
)
nodes_js = to_js(node_list)
edges_js = to_js(edge_list, dict_converter=js.Object.fromEntries)
id_fn = create_proxy(lambda d, *_:d["id"])
highlight_node = None
neighbors = None
circle_mode = False
node_clicked = False
def clicked(event, d):
global highlight_node, neighbors, node_clicked
highlight_node = d["id"]
neighbors = list(G.neighbors(highlight_node))
node_clicked = True
simulation.restart().on("tick", ticked_fn)
clicked_proxy = create_proxy(clicked)
link = (svg
.selectAll("line")
.data(edges_js)
.enter()
.append("line")
.style("stroke", "#aaa")
)
node = (svg
.selectAll(".node")
.data(nodes_js)
.enter()
.append("g")
.attr("class", "node")
.on("click", clicked_proxy)
)
node.append("circle").attr("r", 20).style("fill", "#69b3a2")
node.append("text").attr("text-anchor", "middle").attr("dy", ".35em").text(id_fn)
def highlight(d, *_):
id = d["id"]
if (highlight_node is not None) and (id == highlight_node):
return "#f00"
elif (neighbors is not None) and (int(id) in neighbors):
return "#00f"
else:
return "#000"
def get_cir_coor(id):
node_num = len(node_list)
radius = max_height / 2
rad = 2 * math.pi * (id / node_num)
return radius * math.sin(rad) + width / 2, radius * math.cos(rad) + max_height / 2
def ticked():
if not circle_mode:
(link
.transition().duration(1000)
.attr("x1", create_proxy(lambda d, *_:d.source.x))
.attr("y1", create_proxy(lambda d, *_:d.source.y))
.attr("x2", create_proxy(lambda d, *_:d.target.x))
.attr("y2", create_proxy(lambda d, *_:d.target.y))
)
(node
.transition().duration(1000)
.attr("transform", create_proxy(lambda d, *_:f"translate({d.x},{d.y})"))
.attr("fill", create_proxy(highlight))
)
else:
(link
.transition().duration(1000)
.attr("x1", create_proxy(lambda d, *_:get_cir_coor(d.source["id"])[0]))
.attr("y1", create_proxy(lambda d, *_:get_cir_coor(d.source["id"])[1]))
.attr("x2", create_proxy(lambda d, *_:get_cir_coor(d.target["id"])[0]))
.attr("y2", create_proxy(lambda d, *_:get_cir_coor(d.target["id"])[1]))
)
(node
.transition().duration(1000)
.attr("transform", create_proxy(lambda d, *_:f"translate{get_cir_coor(d['id'])}"))
.attr("fill", create_proxy(highlight))
)
ticked_fn = create_proxy(ticked)
simulation = (d3.forceSimulation(nodes_js)
.force("link", d3.forceLink().id(id_fn).links(edges_js))
.force("charge", d3.forceManyBody().strength(-400))
.force('collide',d3.forceCollide().radius(30).iterations(2))
.force("center", d3.forceCenter(width / 2, max_height / 2))
.on("tick", ticked_fn)
)
def change(event):
global circle_mode, node_clicked
if not node_clicked:
circle_mode = not circle_mode
simulation.restart().on("tick", ticked_fn)
else:
node_clicked = False
change_proxy = create_proxy(change)
graph_ele = js.document.getElementById("viz")
graph_ele.addEventListener("click", change_proxy)