[Python] Trying to create hypergraph with Cytoscape

Discussão em 'Python' iniciado por Stack, Julho 20, 2025.

  1. Stack

    Stack Membro Participativo

    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...

Compartilhe esta Página