animationnetworkx

Taking a complex graph in networkx with edge labels, colors, and weights, and reproducing it step by step


I have a dataset of ordered concept-categories and I'm graphing them by the order that they appear, with labels for conceptual distance and thickness based on times where connections between nodes occur multiple times.

I've got a graph in networkx that I've produced by:

  1. adding edges one at a time; then
  2. adding all of my edge information from a dictionary into labels; then
  3. putting positions onto everything from a dictionary of positions

It took me a lot to figure out how to get the graph right, but I'm now happy with it. My issue is that I want to take a completed graph G and reproduce it one edge at a time so that I can animate it as I go.

This is how I've done it, but it is very glitchy because we get labels appearing before their edges and suchlike. Any idea how I can do this properly?

   ###############################################
    # Doing the bit where we output the video of the graph being constructed

    # Create an empty directed graph for animation
    positions = pos_dic
    G_animated = nx.DiGraph()

    # Initialize figure and axis for animation
    fig, ax = plt.subplots(figsize=(20, 12))
    plt.title(f'Graph for {key} at {timestamp}',font)

    # Step 2: Extract the edges, colors, weights, and labels from the original graph
    edges = list(G.edges(data=True))
    edge_colors = [attr['color'] for u, v, attr in edges]
    edge_weights = [attr['weight'] for u, v, attr in edges]
    edge_labels = {(u, v): attr['label'] for u, v, attr in edges}

    # Step 3: Function to update the graph step by step
    def update_graph(num):
        ax.clear()  # Clear the plot for the next frame
        
        if num < len(edges):
            u, v, attr = edges[num]
            
            # Add the nodes if they don't exist yet in G_animated
            if u not in G_animated.nodes:
                G_animated.add_node(u)
            if v not in G_animated.nodes:
                G_animated.add_node(v)
            
            # Now, add the edge with the attributes
            G_animated.add_edge(u, v, **attr)
        
        # Draw the updated graph with custom positions
        edge_color_list = [edge_colors[i] for i in range(len(G_animated.edges))]
        nx.draw(G_animated, pos=positions, ax=ax, with_labels=False, node_color='lightblue', 
                edge_color=edge_color_list, width=edge_weights[:len(G_animated.edges)], 
                node_size=500, arrows=True)
        nx.draw_networkx_labels(G_animated, pos_dic, font_size=11, font_color='black', font_weight='bold', bbox=dict(facecolor='white', edgecolor='black', boxstyle='round,pad=0.3'))
        # Draw edge labels (showing the label attributes)
        nx.draw_networkx_edge_labels(G_animated, pos=positions, edge_labels=edge_labels)

    # Step 4: Create animation object
    ani = FuncAnimation(fig, update_graph, frames=len(edges), repeat=False, interval=3000)

    # Step 5: Save the animation as a video (e.g., .mp4)
    writer = FFMpegWriter(fps=1, metadata=dict(artist='Nick Kelly'), bitrate=1800)
    ani.save(f"graph_animation_{key}.mp4", writer=writer)

    # plt.show()
    plt.close()

    # End video here
    ##################################################

This gives an animation that is very glitchy, see this file for an example: https://1drv.ms/v/s!AuaSDysD-RqIhOsFSP6pYZCUmvDYvQ?e=iz2k8L


Solution

  • Networkx resizes the axis data limits to fit the plot elements on each draw. As you are expanding the graph, the data limits are changing, giving the impression that graph elements are "jumping" around.

    The simplest fix is to enforce constant data limits at the end of each update:

    ...
    
    xy = np.array(positions.values())
    xmin, ymin = np.min(xy, axis=0)
    xmax, ymax = np.max(xy, axis=0)
    pad_by = 0.05 # may need adjusting 
    pad_x, pad_y = pad_by * np.ptp(xy, axis=0)
    
    def update_graph(num):
        ...
        ax.set_xlim(xmin - pad_x, xmax + pad_x)
        ax.set_ylim(ymin - pad_y, ymax + pad_y)
        ax.set_aspect("equal")
    
    ani = ...