import streamlit as st import matplotlib.pyplot as plt import networkx as nx import bz2 import numpy as np # Sidebar for selecting an option sidebar_option = st.sidebar.radio("Select an option", ["Select an option", "Basic: Properties", "Basic: Read and write graphs", "Basic: Simple graph", "Basic: Simple graph Directed", "Drawing: Custom Node Position", "Drawing: Chess Masters"]) # Helper function to draw and display graph def draw_graph(G, pos=None, title="Graph Visualization"): plt.figure(figsize=(12, 12)) nx.draw_networkx_edges(G, pos, alpha=0.3, width=edgewidth, edge_color="m") nx.draw_networkx_nodes(G, pos, node_size=nodesize, node_color="#210070", alpha=0.9) label_options = {"ec": "k", "fc": "white", "alpha": 0.7} nx.draw_networkx_labels(G, pos, font_size=14, bbox=label_options) # Title/legend font = {"fontname": "Helvetica", "color": "k", "fontweight": "bold", "fontsize": 14} ax = plt.gca() ax.set_title(title, font) ax.text( 0.80, 0.10, "edge width = # games played", horizontalalignment="center", transform=ax.transAxes, fontdict=font, ) ax.text( 0.80, 0.06, "node size = # games won", horizontalalignment="center", transform=ax.transAxes, fontdict=font, ) # Resize figure for label readability ax.margins(0.1, 0.05) plt.axis("off") st.pyplot(plt) def chess_pgn_graph(pgn_file="chess_masters_WCC.pgn.bz2"): """Read chess games in pgn format in pgn_file. Filenames ending in .bz2 will be uncompressed. Return the MultiDiGraph of players connected by a chess game. Edges contain game data in a dict. """ G = nx.MultiDiGraph() game = {} with bz2.BZ2File(pgn_file) as datafile: lines = [line.decode().rstrip("\r\n") for line in datafile] for line in lines: if line.startswith("["): tag, value = line[1:-1].split(" ", 1) game[str(tag)] = value.strip('"') else: if game: white = game.pop("White") black = game.pop("Black") G.add_edge(white, black, **game) game = {} return G # Draw Chess Masters Graph (the main section) def display_chess_masters_graph(): st.title("Drawing: Chess Masters") option = st.radio("Choose a graph type:", ("Default Example", "Create your own")) if option == "Default Example": G = chess_pgn_graph("chess_masters_WCC.pgn.bz2") # identify connected components of the undirected version H = G.to_undirected() Gcc = [H.subgraph(c) for c in nx.connected_components(H)] if len(Gcc) > 1: st.write(f"Note the disconnected component consisting of:\n{Gcc[1].nodes()}") # find all games with B97 opening (as described in ECO) openings = {game_info["ECO"] for (white, black, game_info) in G.edges(data=True)} st.write(f"\nFrom a total of {len(openings)} different openings,") st.write("the following games used the Sicilian opening") st.write('with the Najdorff 7...Qb6 "Poisoned Pawn" variation.\n') for white, black, game_info in G.edges(data=True): if game_info["ECO"] == "B97": summary = f"{white} vs {black}\n" for k, v in game_info.items(): summary += f" {k}: {v}\n" summary += "\n" st.write(summary) # Create undirected graph H without multi-edges H = nx.Graph(G) # Edge width is proportional to number of games played edgewidth = [len(G.get_edge_data(u, v)) for u, v in H.edges()] # Node size is proportional to number of games won wins = dict.fromkeys(G.nodes(), 0.0) for u, v, d in G.edges(data=True): r = d["Result"].split("-") if r[0] == "1": wins[u] += 1.0 elif r[0] == "1/2": wins[u] += 0.5 wins[v] += 0.5 else: wins[v] += 1.0 nodesize = [wins[v] * 50 for v in H] # Generate layout for visualization pos = nx.kamada_kawai_layout(H) # Manually tweak some positions to avoid label overlap pos["Reshevsky, Samuel H"] += (0.05, -0.10) pos["Botvinnik, Mikhail M"] += (0.03, -0.06) pos["Smyslov, Vassily V"] += (0.05, -0.03) # Draw the graph draw_graph(H, pos, title="World Chess Championship Games: 1886 - 1985") elif option == "Create your own": uploaded_file = st.file_uploader("Upload your own PGN file", type="pgn") if uploaded_file is not None: G_custom = chess_pgn_graph(uploaded_file) # Identify connected components and draw the graph for the uploaded data H_custom = G_custom.to_undirected() edgewidth = [len(G_custom.get_edge_data(u, v)) for u, v in H_custom.edges()] wins = dict.fromkeys(G_custom.nodes(), 0.0) for u, v, d in G_custom.edges(data=True): r = d["Result"].split("-") if r[0] == "1": wins[u] += 1.0 elif r[0] == "1/2": wins[u] += 0.5 wins[v] += 0.5 else: wins[v] += 1.0 nodesize = [wins[v] * 50 for v in H_custom] pos_custom = nx.kamada_kawai_layout(H_custom) draw_graph(H_custom, pos_custom, title="Custom Chess Game Graph") # Display other sections def display_basic_properties(): st.title("Basic: Properties") option = st.radio("Choose a graph type:", ("Default Example", "Create your own")) # Default example: 5x5 grid graph if option == "Default Example": G = nx.lollipop_graph(4, 6) display_graph_properties(G) elif option == "Create your own": num_nodes = st.number_input("Number of nodes:", min_value=2, max_value=50, value=5) num_edges = st.number_input("Number of edges per group (for lollipop graph):", min_value=1, max_value=10, value=3) if st.button("Generate"): if num_nodes >= 2 and num_edges >= 1: G_custom = nx.lollipop_graph(num_nodes, num_edges) display_graph_properties(G_custom) def display_graph_properties(G): pathlengths = [] st.write("### Source vertex {target:length, }") for v in G.nodes(): spl = dict(nx.single_source_shortest_path_length(G, v)) st.write(f"Vertex {v}: {spl}") for p in spl: pathlengths.append(spl[p]) avg_path_length = sum(pathlengths) / len(pathlengths) st.write(f"### Average shortest path length: {avg_path_length}") dist = {} for p in pathlengths: if p in dist: dist[p] += 1 else: dist[p] = 1 st.write("### Length #paths") for d in sorted(dist.keys()): st.write(f"Length {d}: {dist[d]} paths") st.write("### Properties") st.write(f"Radius: {nx.radius(G)}") st.write(f"Diameter: {nx.diameter(G)}") st.write(f"Eccentricity: {nx.eccentricity(G)}") st.write(f"Center: {nx.center(G)}") st.write(f"Periphery: {nx.periphery(G)}") st.write(f"Density: {nx.density(G)}") st.write("### Graph Visualization") pos = nx.spring_layout(G, seed=3068) plt.figure(figsize=(8, 6)) nx.draw(G, pos=pos, with_labels=True, node_color='lightblue', node_size=500, font_size=10, font_weight='bold') st.pyplot(plt) # Display other sections def display_read_write_graph(): st.title("Basic: Read and write graphs") G = nx.karate_club_graph() # Write the graph nx.write_gml(G, "karate_club.gml") st.write("Graph written to 'karate_club.gml'.") # Read the graph back G_new = nx.read_gml("karate_club.gml") st.write("Graph read back from 'karate_club.gml'.") nx.draw(G_new, with_labels=True) st.pyplot(plt) def display_simple_graph(): st.title("Basic: Simple graph") G = nx.complete_graph(5) nx.draw(G, with_labels=True) st.pyplot(plt) def display_simple_directed_graph(): st.title("Basic: Simple graph Directed") G = nx.complete_graph(5, nx.DiGraph()) nx.draw(G, with_labels=True) st.pyplot(plt) def display_custom_node_position(): st.title("Drawing: Custom Node Position") pos = {"A": (1, 2), "B": (2, 3), "C": (3, 1)} G = nx.Graph(pos) nx.draw(G, pos=pos, with_labels=True) st.pyplot(plt) # Call the appropriate function based on sidebar selection if sidebar_option == "Basic: Properties": display_basic_properties() elif sidebar_option == "Basic: Read and write graphs": display_read_write_graph() elif sidebar_option == "Basic: Simple graph": display_simple_graph() elif sidebar_option == "Basic: Simple graph Directed": display_simple_directed_graph() elif sidebar_option == "Drawing: Custom Node Position": display_custom_node_position() elif sidebar_option == "Drawing: Chess Masters": display_chess_masters_graph() else: st.write("Please select a valid option from the sidebar.")