{ "packages": ["networkx"] }
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)