Sirui W
Sirui W

Reputation: 21

Python Networkx graph on x, y axis and having the edge attribute plot on the z axis matching the Networkx graph edges

I have a 2D directional networkx graph representing underground pipeline networks, where the edge of the Networkx graph represent a single pipeline. I have set the attribute of each edge in the graph to be a NumPy array containing the following pressure distribution.

Length (each pipe) Pressure
0 100
10 90
20 80
... ...

I am trying to visualize all pipelines' internal pressure distribution within the same figure plot as the networkx graph. So, the ideal outcome is a 3D figure plot, where the x, y axis is the Networkx graph, and the z axis on each of the edge plotted in the x, y axis is the internal pressure distribution.

I have hand draw the figure with what I have in mind.

enter image description here

The following code is an example of my network graph with an edge attribute being a 2d NumPy array.

If there is a way to animate such drawing with changing pressure distribution will be even better.

import numpy as np
import pandas as pd
import networkx as nx
import matplotlib.pyplot as plt

Sections = np.array([0, 1, 2, 3, 4])
Sources = np.array(["A", "B", "B", "D", "E"])
Targets = np.array(["B", "C", "D", "E", "F"])
Sections_L = np.array([250, 250, 100, 250, 250])
Sections_a = np.full(len(Sections), 1000)
Sections_D = np.array([0.173, 0.173, 0.1, 0.173, 0.173])
Sections_f = np.array([0.047, 0.047, 0.057, 0.047, 0.047])
Sections_U = np.array([0.85, 0.85, 2.55, 0.85, 0.85])
Sections_A = (np.pi * Sections_D ** 2) / 4
Sections_Q0 = Sections_A * Sections_U
Sections_S = np.zeros(len(Sections))
Data = np.column_stack(
    (Sections_L, Sections_D, Sections_A, Sections_a, Sections_f, Sections_U, Sections_Q0, Sections_S, Sources,
     Targets))
PipePD = pd.DataFrame(data=Data, index=Sections,
                      columns=["L", "D", "A", "a", "f", "U", "Q0", "isSource", "sources", "targets"])
G = nx.from_pandas_edgelist(PipePD, source="sources", target="targets", edge_attr=True, create_using=nx.DiGraph())

for edge in G.edges:
    length = int(G.edges[edge]["L"])
    lengthDist = range(0, length, 1)
    dist = np.random.uniform (0,10, len(lengthDist))
    PressureDist = np.column_stack((lengthDist, dist))
    G.edges[edge]["PressureDist"] = PressureDist


# plt.figure(1)
# edge_labels = {i[0:2]: '{}m'.format(i[2]['L']) for i in G.edges(data=True)}
# pos = nx.spring_layout(G, weight="L")
# nx.draw(G, pos, with_labels=True)
# nx.draw_networkx_edge_labels(G, pos, edge_labels)
# plt.show()

Upvotes: 2

Views: 241

Answers (1)

Paul Brodersen
Paul Brodersen

Reputation: 13021

Personally, I would draw the network in 2D and use color to denote pressure. However, you absolutely can use the z-axis instead. Networkx doesn't support custom edge paths, so you have to compute and draw them yourself.

enter image description here

import numpy as np
import pandas as pd
import networkx as nx
import matplotlib.pyplot as plt

Sections = np.array([0, 1, 2, 3, 4])
Sources = np.array(["A", "B", "B", "D", "E"])
Targets = np.array(["B", "C", "D", "E", "F"])
Sections_L = np.array([250, 250, 100, 250, 250])
Sections_a = np.full(len(Sections), 1000)
Sections_D = np.array([0.173, 0.173, 0.1, 0.173, 0.173])
Sections_f = np.array([0.047, 0.047, 0.057, 0.047, 0.047])
Sections_U = np.array([0.85, 0.85, 2.55, 0.85, 0.85])
Sections_A = (np.pi * Sections_D ** 2) / 4
Sections_Q0 = Sections_A * Sections_U
Sections_S = np.zeros(len(Sections))
# This casts everything to strings!
# Data = np.column_stack(
#     (Sections_L, Sections_D, Sections_A, Sections_a, Sections_f, Sections_U, Sections_Q0, Sections_S, Sources,
#      Targets))
# Create a data dictionary instead:
data = (Sections_L, Sections_D, Sections_A, Sections_a, Sections_f, Sections_U, Sections_Q0, Sections_S, Sources, Targets)
columns = ["L", "D", "A", "a", "f", "U", "Q0", "isSource", "sources", "targets"]
PipePD = pd.DataFrame(data=dict(zip(columns, data)), index=Sections)
PipePD['L_inverse'] = 1. / PipePD['L']

G = nx.from_pandas_edgelist(PipePD, source="sources", target="targets", edge_attr=True, create_using=nx.DiGraph())

for edge in G.edges:
    length = int(G.edges[edge]["L"])
    length_along_edge = range(0, length, 1)
    pressure = np.random.uniform(0,10, len(length_along_edge))
    # Smooth if you want to:
    # from statsmodels.nonparametric.smoothers_lowess import lowess
    # length_along_edge, pressure = lowess(pressure, length_along_edge, 0.1).T
    G.edges[edge]["PressureDist"] = np.column_stack((length_along_edge, pressure))

fig = plt.figure()
ax = fig.add_subplot(projection='3d')
pos = nx.spring_layout(G, weight='L_inverse') # edges with high weight are short; you want long edges to be long (presumably)
edge_to_line = dict()
for (source, target) in G.edges:
    delta = pos[target] - pos[source]
    data = G.get_edge_data(source, target)
    total_length = data['L']
    length_along_edge, z = data['PressureDist'].T
    xy = (length_along_edge / total_length)[:,np.newaxis] * delta[np.newaxis,:] + pos[source]
    x, y = xy.T
    line, = ax.plot(x, y, z)
    edge_to_line[(source, target)] = line # save reference for easy access in animation
plt.show()

To animate the plot, you can manipulate the x, y, z coordinates of each line in your update function using the set_data_3d line method.

Code not tested:

frame_data = ... # e.g. a list of dictionaries, one for each frame, mapping edges to z values
def update(frame_index):
    artists_to_update = []
    for edge, z in frame_data[frame_index].items():
        line = edge_to_line[edge]
        x, y, _ = line.get_data_3d()
        line.set_data_3d(x, y, z)
        artists_to_update.append(line)
    return artists_to_update

from matplotlib.animation import FuncAnimation
FuncAnimation(fig, update, frames=range(len(frame_data)))
 

Upvotes: 1

Related Questions