i've been trying to create a library to visualize hypergraphs which are also interactive. i've tried various libs i finally went with dash and cytoscape. the image i provided.. output.png its what i get in dash browser which is slightly close to what i want which is download.png. i have a problem tho.. it draws the nodes as polygon points instead of putting them in the shape and close the hyperedge around them. i'm not a professional python programmer i been getting help from AI here and there and searched alot.. but fail to figure whats the problem.. i appreciate if someone check the output and the code below and let me know where i'm going wrong.. thank you output.png download.pg import argparse import random import dash from dash import html from dash.dependencies import Input, Output import dash_cytoscape as cyto import hypernetx as hnx from core.hypergraph import Hypergraph as CoreHypergraph from core.layered_hypergraph import LayeredHyperGraph from algorithms.centrality import degree_centrality from algorithms.clustering import spectral_clustering import networkx as nx import numpy as np from scipy.spatial import ConvexHull def get_color_palette(n): # Use a set of visually distinct colors (can be extended) palette = [ "#e41a1c", "#377eb8", "#4daf4a", "#984ea3", "#ff7f00", "#ffff33", "#a65628", "#f781bf", "#999999", "#66c2a5", "#fc8d62", "#8da0cb", "#e78ac3", "#a6d854", "#ffd92f", "#e5c494", "#b3b3b3", ] # Repeat if not enough colors if n > len(palette): random.seed(42) palette += [ "#" + "".join([random.choice("0123456789ABCDEF") for _ in range(6)]) for _ in range(n - len(palette)) ] return palette[:n] class HypergraphVisualizer: def __init__(self, json_file_path): self.json_file_path = json_file_path self.hyperedges = self.load_hyperedges_from_json() self.core_hypergraph = self.create_core_hypergraph() self.hnx_hypergraph = self.create_hnx_hypergraph() self.centrality, self.clusters = self.compute_metrics() self.node_positions = self.get_node_positions() self.layered_hypergraph = self.create_layered_hypergraph() def load_hyperedges_from_json(self): with open(self.json_file_path, "r") as file: data = json.load(file) hyperedges = {} all_nodes = set() for edge in data: edge_id = edge["id"] head_nodes = [node["id"] for node in edge["head_hyper_nodes"]] tail_nodes = ( [node["id"] for node in edge["tail_hyper_nodes"]] if edge["tail_hyper_nodes"] else [] ) layer = edge.get("layer", 0) hyperedges[edge_id] = { "nodes": set(head_nodes + tail_nodes), "head": set(head_nodes), "tail": set(tail_nodes), "layer": layer, "directed": edge["directed"], } all_nodes.update(head_nodes + tail_nodes) self.all_nodes = all_nodes return hyperedges def create_core_hypergraph(self): H = CoreHypergraph() for edge_id, edge_data in self.hyperedges.items(): H.add_edge(edge_id, edge_data["nodes"]) return H def create_layered_hypergraph(self): LH = LayeredHyperGraph() for edge_id, edge_data in self.hyperedges.items(): layer = edge_data["layer"] LH.add_edge(edge_id, edge_data["nodes"], layer) return LH def compute_metrics(self): centrality = degree_centrality(self.core_hypergraph) clusters = spectral_clustering(self.core_hypergraph, num_clusters=2) return centrality, clusters def create_hnx_hypergraph(self): edge_dict = { edge_id: list(edge_data["nodes"]) for edge_id, edge_data in self.hyperedges.items() } return hnx.Hypergraph(edge_dict) def scale_positions(self, pos, scale=500, center=(500, 300), offset=30): xs = [x for x, y in pos.values()] ys = [y for x, y in pos.values()] min_x, max_x = min(xs), max(xs) min_y, max_y = min(ys), max(ys) def transform(x, y): nx = (x - min_x) / (max_x - min_x) if max_x > min_x else 0.5 ny = (y - min_y) / (max_y - min_y) if max_y > min_y else 0.5 return (center[0] + scale * (nx - 0.5) + offset, center[1] + scale * (ny - 0.5) + offset) return {n: transform(x, y) for n, (x, y) in pos.items()} def get_node_positions(self): # Convert hypergraph to a NetworkX graph for force-directed layout G = nx.Graph() for edge_id, edge_data in self.hyperedges.items(): nodes = list(edge_data["nodes"]) for i in range(len(nodes)): for j in range(i + 1, len(nodes)): G.add_edge(nodes, nodes[j]) # Compute positions using spring layout pos = nx.spring_layout(G, seed=42, k=0.5, iterations=100) return self.scale_positions(pos) def visualize_hypergraph(self): elements = [] edge_ids = list(self.hyperedges.keys()) color_palette = get_color_palette(len(edge_ids)) edge_colors = {eid: color_palette for i, eid in enumerate(edge_ids)} # Add nodes with offset inside hyperedges for node in self.all_nodes: x, y = self.node_positions.get(node, (random.uniform(-100, 100), random.uniform(-100, 100))) elements.append( { "data": {"id": node, "label": node}, "position": {"x": x, "y": y}, "classes": "node", "grabbable": True, } ) # Draw hyperedges as closed polygons with nodes inside for edge_idx, (edge_id, edge_data) in enumerate(self.hyperedges.items()): nodes = list(edge_data["nodes"]) if len(nodes) >= 2: # Calculate centroid and adjust node positions inward x_coords = [self.node_positions[n][0] for n in nodes] y_coords = [self.node_positions[n][1] for n in nodes] centroid_x = np.mean(x_coords) centroid_y = np.mean(y_coords) radius = max(np.max([abs(x - centroid_x) for x in x_coords]), np.max([abs(y - centroid_y) for y in y_coords])) + 50 # Adjust node positions inward by a small offset for node in nodes: node_x, node_y = self.node_positions[node] dx = (node_x - centroid_x) * 0.7 # Move 30% inward dy = (node_y - centroid_y) * 0.7 self.node_positions[node] = (centroid_x + dx, centroid_y + dy) # Draw closed polygon for hyperedge for i in range(len(nodes)): source = nodes target = nodes[(i + 1) % len(nodes)] elements.append( { "data": { "source": source, "target": target, "id": f"{edge_id}_edge_{i}", "label": edge_id if i == 0 else "", }, "classes": f"hyperedge-{edge_id} hull-edge", } ) return elements, edge_colors def main(): parser = argparse.ArgumentParser(description="Hypergraph and Layered Hypergraph Visualizer") parser.add_argument("json_file", help="Path to the JSON file containing hypergraph data") parser.add_argument("--layered", action="store_true", help="Visualize layered hypergraph only") args = parser.parse_args() visualizer = HypergraphVisualizer(args.json_file) app = dash.Dash(__name__) if args.layered: elements, edge_colors = visualizer.visualize_layered_hypergraph() else: elements, edge_colors = visualizer.visualize_hypergraph() # Build stylesheet stylesheet = [ { "selector": ".node", "style": { "label": "data(label)", "background-color": "black", "width": "8px", "height": "8px", "text-valign": "top", "text-margin-y": "20px", # Increased margin for better readability "z-index": 10, }, }, { "selector": ".hull-edge", "style": { "width": 2, "line-color": "data(label)", "curve-style": "unbundled-bezier", "z-index": 1, }, }, ] for edge_id, color in edge_colors.items(): stylesheet.append( { "selector": f".hyperedge-{edge_id}", "style": { "line-color": color, "opacity": 0.7, }, } ) app.layout = html.Div( [ cyto.Cytoscape( id="cytoscape-hypergraph", elements=elements, style={"width": "100%", "height": "800px"}, layout={"name": "preset", "padding": 10}, stylesheet=stylesheet, userZoomingEnabled=True, userPanningEnabled=True, autoRefreshLayout=True, ), html.Div(id="output"), html.Div( [ html.H4("Hyperedge Colors:"), # Larger header html.Ul( [ html.Li( [ html.Span( style={ "display": "inline-block", "width": "30px", # Larger color box "height": "15px", # Larger height "backgroundColor": color, "marginRight": "15px", } ), edge_id, ], style={"fontSize": "16px"} # Larger text ) for edge_id, color in edge_colors.items() ], style={"fontSize": "16px"} # Larger list text ), ], style={ "position": "absolute", "top": "10px", "right": "10px", # Top-right corner "marginTop": "0", "backgroundColor": "#f9f9f9", "padding": "10px", "border": "1px solid #ddd", }, ), ] ) @app.callback( Output("output", "children"), Input("cytoscape-hypergraph", "tapNodeData") ) def display_node_info(data): if data and "id" in data: node_id = data["id"].split("_layer")[0] centrality = visualizer.centrality.get(node_id, 0) cluster = visualizer.clusters.get(node_id, 0) return html.Div( [ html.H4(f"Node: {node_id}"), html.P(f"Centrality: {centrality:.2f}"), html.P(f"Cluster: {cluster}"), ] ) return "" if args.layered: pass else: app.run(debug=True) if __name__ == "__main__": main()``` Continue reading...