Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to draw "two directions widths line" in matplotlib

How to use matplotlib or pyqtgraph draw plot like this: two dirrections widths line

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)

enter image description here

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!

like image 621
Macer Avatar asked Jun 10 '16 04:06

Macer


People also ask

How do I change the width of a line in Matplotlib?

Import the various modules and libraries you need for the plot: matplot library matplot , numpy , pyplot . Create your data set(s) to be plotted. In the plot() method after declaring the linewidth parameter, you assign it to any number value you want to represent the desired width of your plot.

How do you change the width of a line in Python?

You can use the keyword argument linewidth or the shorter lw to change the width of the line.


2 Answers

If you can have each independent line, this can be done easily with the fill_between function.

from matplotlib import pyplot as plt
import numpy as np

x=np.array([0,1,2,3])
y=np.array([1,0,0,-1])

y1width=-1
y2width=3
y1 = y + y1width
y2 = y + y2width

fig = plt.figure()
ax = fig.add_subplot(111)

plt.plot(x,y, 'k', x,y1, 'k',x,y2, 'k',linewidth=2)
ax.fill_between(x, y1, y, color='g')
ax.fill_between(x, y2, y, color='r')

plt.xlim(-1,4)
plt.ylim(-3,6)
plt.show()

Here I considered the center line as the reference (thus the negative y1width), but could be done differently. The result is then:

<code>fill_between</code> result.

If the lines are 'complicated', eventually intersecting at some point, then the keyword argument interpolate=True must be used to fill the crossover regions properly. Another interesting argument probably useful for your use case is where, to condition the region, for instance, where=y1 < 0. For more information you can check out the documentation.

like image 92
rll Avatar answered Sep 19 '22 17:09

rll


One way of solving your issue is using filled polygons, some linear algebra and some calculus. The main idea is to draw a polygon along your x and y coordinates and along shifted coordinates to close and fill the polygon.

These are my results: Filled polygons along path

And here is the code:

from __future__ import division
import numpy
from matplotlib import pyplot, patches


def road(x, y, w, scale=0.005, **kwargs):
    # Makes sure input coordinates are arrays.
    x, y = numpy.asarray(x, dtype=float), numpy.asarray(y, dtype=float)
    # Calculate derivative.
    dx = x[2:] - x[:-2]
    dy = y[2:] - y[:-2]
    dy_dx = numpy.concatenate([
        [(y[1] - y[0]) / (x[1] - x[0])],
        dy / dx,
        [(y[-1] - y[-2]) / (x[-1] - x[-2])]
    ])
    # Offsets the input coordinates according to the local derivative.
    offset = -dy_dx + 1j
    offset =  w * scale * offset / abs(offset)
    y_offset = y + w * scale
    #
    AB = zip(
        numpy.concatenate([x + offset.real, x[::-1]]),
        numpy.concatenate([y + offset.imag, y[::-1]]),
    )
    p = patches.Polygon(AB, **kwargs)

    # Returns polygon.
    return p


if __name__ == '__main__':
    # Some plot initializations
    pyplot.close('all')
    pyplot.ion()

    # This is the list of coordinates of each point
    x = [0, 1, 2, 3, 4]
    y = [1, 0, 0, -1, 0]

    # Creates figure and axes.
    fig, ax = pyplot.subplots(1,1)
    ax.axis('equal')
    center_line, = ax.plot(x, y, color='k', linewidth=2)

    AB = road(x, y, 20, color='g')
    BA = road(x, y, -30, color='r')
    ax.add_patch(AB)
    ax.add_patch(BA)

The first step in calculating how to offset each data point is by calculating the discrete derivative dy / dx. I like to use complex notation to handle vectors in Python, i.e. A = 1 - 1j. This makes life easier for some mathematical operations.

The next step is to remember that the derivative gives the tangent to the curve and from linear algebra that the normal to the tangent is n=-dy_dx + 1j, using complex notation.

The final step in determining the offset coordinates is to ensure that the normal vector has unity size n_norm = n / abs(n) and multiply by the desired width of the polygon.

Now that we have all the coordinates for the points in the polygon, the rest is quite straightforward. Use patches.Polygon and add them to the plot.

This code allows you also to define if you want the patch on top of your route or below it. Just give a positive or negative value for the width. If you want to change the width of the polygon depending on your zoom level and/or resolution, you adjust the scale parameter. It also gives you freedom to add additional parameters to the patches such as fill patterns, transparency, etc.

like image 42
regeirk Avatar answered Sep 19 '22 17:09

regeirk