I am trying to plot an outline (linestyle=":"
) on the networkx
edges. I can't seem to figure out how to do this to the matplotlib
patch
objects? Does anyone now how to manipulate these patch
object to plot outlines to these "edges"? If this is not possible, does anyone know how to get the line data to use ax.plot(x,y,linestyle=":")
separately to do this?
import networkx as nx
import numpy as np
from collections import *
# Graph data
G = {'y1': OrderedDict([('y2', OrderedDict([('weight', 0.8688325076457851)])), (1, OrderedDict([('weight', 0.13116749235421485)]))]), 'y2': OrderedDict([('y3', OrderedDict([('weight', 0.29660515972204304)])), ('y4', OrderedDict([('weight', 0.703394840277957)]))]), 'y3': OrderedDict([(4, OrderedDict([('weight', 0.2858185316736193)])), ('y5', OrderedDict([('weight', 0.7141814683263807)]))]), 4: OrderedDict(), 'input': OrderedDict([('y1', OrderedDict([('weight', 1.0)]))]), 'y4': OrderedDict([(3, OrderedDict([('weight', 0.27847763084646443)])), (5, OrderedDict([('weight', 0.7215223691535356)]))]), 3: OrderedDict(), 5: OrderedDict(), 'y5': OrderedDict([(6, OrderedDict([('weight', 0.5733512797415756)])), (2, OrderedDict([('weight', 0.4266487202584244)]))]), 6: OrderedDict(), 1: OrderedDict(), 2: OrderedDict()}
G = nx.from_dict_of_dicts(G)
G_scaffold = {'input': OrderedDict([('y1', OrderedDict())]), 'y1': OrderedDict([('y2', OrderedDict()), (1, OrderedDict())]), 'y2': OrderedDict([('y3', OrderedDict()), ('y4', OrderedDict())]), 1: OrderedDict(), 'y3': OrderedDict([(4, OrderedDict()), ('y5', OrderedDict())]), 'y4': OrderedDict([(3, OrderedDict()), (5, OrderedDict())]), 4: OrderedDict(), 'y5': OrderedDict([(6, OrderedDict()), (2, OrderedDict())]), 3: OrderedDict(), 5: OrderedDict(), 6: OrderedDict(), 2: OrderedDict()}
G_scaffold = nx.from_dict_of_dicts(G_scaffold)
G_sem = {'y1': OrderedDict([('y2', OrderedDict([('weight', 0.046032370518141796)])), (1, OrderedDict([('weight', 0.046032370518141796)]))]), 'y2': OrderedDict([('y3', OrderedDict([('weight', 0.08764771571290508)])), ('y4', OrderedDict([('weight', 0.08764771571290508)]))]), 'y3': OrderedDict([(4, OrderedDict([('weight', 0.06045928834718992)])), ('y5', OrderedDict([('weight', 0.06045928834718992)]))]), 4: OrderedDict(), 'input': OrderedDict([('y1', OrderedDict([('weight', 0.0)]))]), 'y4': OrderedDict([(3, OrderedDict([('weight', 0.12254141747735424)])), (5, OrderedDict([('weight', 0.12254141747735425)]))]), 3: OrderedDict(), 5: OrderedDict(), 'y5': OrderedDict([(6, OrderedDict([('weight', 0.11700701511079069)])), (2, OrderedDict([('weight', 0.11700701511079069)]))]), 6: OrderedDict(), 1: OrderedDict(), 2: OrderedDict()}
G_sem = nx.from_dict_of_dicts(G_sem)
# Edge info
edge_input = ('input', 'y1')
weights_sem = np.array([G_sem[u][v]['weight']for u,v in G_sem.edges()]) * 256
# Layout
pos = nx.nx_agraph.graphviz_layout(G_scaffold, prog="dot", root="input")
# Plotting graph
pad = 10
with plt.style.context("ggplot"):
fig, ax = plt.subplots(figsize=(8,8))
linecollection = nx.draw_networkx_edges(G_sem, pos, alpha=0.5, edges=G_sem.edges(), arrowstyle="-", edge_color="#000000", width=weights_sem)
x = np.stack(pos.values())[:,0]
y = np.stack(pos.values())[:,1]
ax.set(xlim=(x.min()-pad,x.max()+pad), ylim=(y.min()-pad, y.max()+pad))
for path, lw in zip(linecollection.get_paths(), linecollection.get_linewidths()):
x = path.vertices[:,0]
y = path.vertices[:,1]
w = lw/4
theta = np.arctan2(y[-1] - y[0], x[-1] - x[0])
# ax.plot(x, y, color="blue", linestyle=":")
ax.plot((x-np.sin(theta)*w), y+np.cos(theta)*w, color="blue", linestyle=":")
ax.plot((x+np.sin(theta)*w), y-np.cos(theta)*w, color="blue", linestyle=":")
After a couple of thought experiments, I realized I need to calculate the angle and then adjust the pads accordingly:
For example, if the line was completely vertical (at 90 or -90) then the y coords would not be shifted at all by the x coords would be shifted. The opposite would happen for a line with an angle 0 or 180.
However, it's still off a bit.
I suspect that this is relevant: matplotlib - Expand the line with specified width in data unit?
I don't think the linewidth
directly translates to data space
Alternatively, if these line collections could be converted into rectangle objects then it would also be possible.
The problem of surrounding a line with a certain width by another line is that the line is defined in data coordinates, while the linewidth is in a physical unit, namely points. This is in general desireable, because it allows to have the linewidth to be independent of the data range, zooming level etc. It also ensures that the end of the line is always perpendicular to the line, independent of the axes aspect.
So the outline of the line is always in a mixed coordinate system and the final appearance is not determined before drawing the actual line with the renderer. So for a solution that takes the (possibly changing) coordinates into account, one would need to determine the outline for the current state of the figure.
One option is to use a new artist, which takes the existing LineCollection
as input and creates new transforms depending on the current position of the lines in pixel space.
In the following I chose a PatchCollection
. Starting off with a rectangle, we can scale and rotate it and then translate it to the position of the original line.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection, PatchCollection
import matplotlib.transforms as mtrans
class OutlineCollection(PatchCollection):
def __init__(self, linecollection, ax=None, **kwargs):
self.ax = ax or plt.gca()
self.lc = linecollection
assert np.all(np.array(self.lc.get_segments()).shape[1:] == np.array((2,2)))
rect = plt.Rectangle((-.5, -.5), width=1, height=1)
super().__init__((rect,), **kwargs)
self.set_transform(mtrans.IdentityTransform())
self.set_offsets(np.zeros((len(self.lc.get_segments()),2)))
self.ax.add_collection(self)
def draw(self, renderer):
segs = self.lc.get_segments()
n = len(segs)
factor = 72/self.ax.figure.dpi
lws = self.lc.get_linewidth()
if len(lws) <= 1:
lws = lws*np.ones(n)
transforms = []
for i, (lw, seg) in enumerate(zip(lws, segs)):
X = self.lc.get_transform().transform(seg)
mean = X.mean(axis=0)
angle = np.arctan2(*np.squeeze(np.diff(X, axis=0))[::-1])
length = np.sqrt(np.sum(np.diff(X, axis=0)**2))
trans = mtrans.Affine2D().scale(length,lw/factor).rotate(angle).translate(*mean)
transforms.append(trans.get_matrix())
self._transforms = transforms
super().draw(renderer)
Note how the actual transforms are only calculated at draw
time. This ensures that they take the actual positions in pixel space into account.
Usage could look like
verts = np.array([[[5,10],[5,5]], [[5,5],[8,2]], [[5,5],[1,4]], [[1,4],[2,0]]])
plt.rcParams["axes.xmargin"] = 0.1
fig, (ax1, ax2) = plt.subplots(ncols=2, sharex=True, sharey=True)
lc1 = LineCollection(verts, color="k", alpha=0.5, linewidth=20)
ax1.add_collection(lc1)
olc1 = OutlineCollection(lc1, ax=ax1, linewidth=2,
linestyle=":", edgecolor="black", facecolor="none")
lc2 = LineCollection(verts, color="k", alpha=0.3, linewidth=(10,20,40,15))
ax2.add_collection(lc2)
olc2 = OutlineCollection(lc2, ax=ax2, linewidth=3,
linestyle="--", edgecolors=["r", "b", "gold", "indigo"],
facecolor="none")
for ax in (ax1,ax2):
ax.autoscale()
plt.show()
Now of course the idea is to use the linecollection
object from the question instead of the lc1
object from the above. This should be easy enough to replace in the code.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With