How to use matplotlib or pyqtgraph draw plot like this:
Line AB is a two-directions street, green part represents the direction from point A to point B, red part represents B to A, width of each part represents the traffic volume. Widths are measured in point, will not changed at different zoom levels or dpi settings.
This is only an example, in fact I have hunderds of streets. This kind of plot is very common in many traffic softwares. I tried to use matplotlib's patheffect but result is frustrated:
from matplotlib import pyplot as plt
import matplotlib.patheffects as path_effects
x=[0,1,2,3]
y=[1,0,0,-1]
ab_width=20
ba_width=30
fig, axes= plt.subplots(1,1)
center_line, = axes.plot(x,y,color='k',linewidth=2)
center_line.set_path_effects(
[path_effects.SimpleLineShadow(offset=(0, -ab_width/2),shadow_color='g', alpha=1, linewidth=ab_width),
path_effects.SimpleLineShadow(offset=(0, ba_width/2), shadow_color='r', alpha=1, linewidth=ba_width),
path_effects.SimpleLineShadow(offset=(0, -ab_width), shadow_color='k', alpha=1, linewidth=2),
path_effects.SimpleLineShadow(offset=(0, ba_width), shadow_color='k', alpha=1, linewidth=2),
path_effects.Normal()])
axes.set_xlim(-1,4)
axes.set_ylim(-1.5,1.5)
One idea came to me is to take each part of the line as a standalone line, and recalculate it's position when changing zoom level, but it's too complicated and slow.
If there any easy way to use matplotlib or pyqtgraph draw what I want? Any suggestion will be appreciated!
Many years later, I figured it out! Do do it, one has to create a new Path Effect which performs the necessary rendering:
class MultilinePath(AbstractPathEffect):
def __init__(self, offset=(0, 0), part_widths=(1,), part_colors=('b',), **kwargs):
super().__init__(offset=(0, 0))
self._path_colors = [mcolors.to_rgba(color) for color in part_colors]
self._path_widths = np.array(part_widths)
self._path_offsets = np.cumsum(self._path_widths) - self._path_widths/2 - np.sum(self._path_widths)/2
self._gc = kwargs
def draw_path(self, renderer, gc, tpath, affine, rgbFace=None):
"""
Overrides the standard draw_path to instead draw several paths slightly offset
"""
gc0 = renderer.new_gc() # Don't modify gc, but a copy!
gc0.copy_properties(gc)
gc0 = self._update_gc(gc0, self._gc)
offset_px = renderer.points_to_pixels(self._path_offsets*gc0.get_linewidth())
base_linewidth = renderer.points_to_pixels(gc0.get_linewidth())
# Transform before evaluation because to_polygons works at resolution
# of one -- assuming it is working in pixel space.
transpath = affine.transform_path(tpath)
# Evaluate to straight line segments to we can get the normal
polys = transpath.to_polygons(closed_only=False)
for p in polys:
x = p[:, 0]
y = p[:, 1]
# Can not interpolate points or draw line if only one point in
# polyline.
if x.size < 2:
continue
dx = np.concatenate([[x[1] - x[0]], x[2:] - x[:-2], [x[-1] - x[-2]]])
dy = np.concatenate([[y[1] - y[0]], y[2:] - y[:-2], [y[-1] - y[-2]]])
l = np.hypot(dx, dy)
nx = dy / l
ny = -dx / l
for i, width in enumerate(self._path_widths):
xyt = np.copy(p)
xyt[:, 0] += nx * offset_px[i]
xyt[:, 1] += ny * offset_px[i]
h = Path(xyt)
# Transform back to data space during render
gc0.set_linewidth(width * base_linewidth)
gc0.set_foreground(self._path_colors[i])
renderer.draw_path(gc0, h, trf.IdentityTransform(), rgbFace)
gc0.restore()
And then you can just apply it to any path rendered by matplotlib. For example:
nx = 101
x = np.linspace(0.0, 1.0, nx)
y = 0.3*np.sin(x*8) + 0.4
ax.plot(x, y, label="Curve", path_effects=[MultilinePath(part_widths=[1, 1, 1], part_colors=['r', 'g', 'b'])])
plt.show()