Matplotlib plot with variable line width
Asked Answered
K

4

47

Is it possible to plot a line with variable line width in matplotlib? For example:

from pylab import *
x = [1, 2, 3, 4, 5]
y = [1, 2, 2, 0, 0]
width = [.5, 1, 1.5, .75, .75]

plot(x, y, linewidth=width)

This doesn't work because linewidth expects a scalar.

Note: I'm aware of *fill_between()* and *fill_betweenx()*. Because these only fill in x or y direction, these do not do justice to cases where you have a slanted line. It is desirable for the fill to always be normal to the line. That is why a variable width line is sought.

Kellie answered 15/10, 2013 at 20:55 Comment(0)
B
94

Use LineCollections. A way to do it along the lines of this Matplotlib example is

import numpy as np
from matplotlib.collections import LineCollection
import matplotlib.pyplot as plt
x = np.linspace(0,4*np.pi,10000)
y = np.cos(x)
lwidths=1+x[:-1]
points = np.array([x, y]).T.reshape(-1, 1, 2)
segments = np.concatenate([points[:-1], points[1:]], axis=1)
lc = LineCollection(segments, linewidths=lwidths,color='blue')
fig,a = plt.subplots()
a.add_collection(lc)
a.set_xlim(0,4*np.pi)
a.set_ylim(-1.1,1.1)
fig.show()

output

Binoculars answered 9/12, 2013 at 15:51 Comment(5)
Nice! So you cut the line into a series of pieces and use LineCollection to specify the properties in each piece.Kellie
Not at all what I was looking for, but this is pretty cool, so I up voted :)Tarbox
Learning curve is steep with matplotlib - trying to figure out how to adapt this to a graph where the x-axis contains timestamps and apparently segments expects float, not timestamp... any clues? This is otherwise exactly what I'm looking for, except for the inability to actually produce a graph with it...Alda
I would suggest to post a new question, linking to this question as a referenceBinoculars
Coming to this much later, a hugely helpful answer!. To reply to the comment from @Alda about using timestamps (which I am doing currently), I would replace the timestamps with a number list then use something like ax.set_xticks(ticks=[0,60,120,180], labels=['09:00', '09:01', '09:02', '09:03'], rotation=45). This would use data for every second and label every minute, and angle the labels by 45 degrees to reduce overlap. Obviously you may need to use list slicing and/or datetime.strftime() formatting to get your positions and labels looking the way you want themAnissaanita
M
10

An alternative to Giulio Ghirardo's answer which divides the lines in segments you can use matplotlib's in-built scatter function which construct the line by using circles instead:

from matplotlib import pyplot as plt
import numpy as np

x = np.linspace(0,10,10000)
y = 2 - 0.5*np.abs(x-4)
lwidths = (1+x)**2 # scatter 'o' marker size is specified by area not radius 
plt.scatter(x,y, s=lwidths, color='blue')
plt.xlim(0,9)
plt.ylim(0,2.1)
plt.show()

In my experience I have found two problems with dividing the line into segments:

  1. For some reason the segments are always divided by very thin white lines. The colors of these lines get blended with the colors of the segments when using a very large amount of segments. Because of this the color of the line is not the same as the intended one.

  2. It doesn't handle very well very sharp discontinuities.

Molnar answered 29/11, 2014 at 17:42 Comment(1)
Regarding your problem (1) and maybe your problem (2) with Gulio's answer, I found a workaround passing the argument antialiaseds=False to LineCollection .Heavyladen
O
0

You can plot each segment of the line separately, with its separate line width, something like:

from pylab import *
x = [1, 2, 3, 4, 5]
y = [1, 2, 2, 0, 0]
width = [.5, 1, 1.5, .75, .75]

for i in range(len(x)-1):
    plot(x[i:i+2], y[i:i+2], linewidth=width[i])
show()
Octillion answered 15/10, 2013 at 21:15 Comment(1)
While this works, it has two problems: 1) For large data sets (e.g. 10,000 points), this creates about the same number of line objects, which is a burden to render. 2) The connections don't look good, since they are made up of overlapping rectangular corners.Kellie
A
0

gg349's answer works nicely but cuts the line into many pieces, which can often creates bad rendering.

Here is an alternative example that generates continuous lines when the width is homogeneous:

import numpy as np
import matplotlib.pyplot as plt

fig, ax = plt.subplots(1)
xs = np.cos(np.linspace(0, 8 * np.pi, 200)) * np.linspace(0, 1, 200)
ys = np.sin(np.linspace(0, 8 * np.pi, 200)) * np.linspace(0, 1, 200)
widths = np.round(np.linspace(1, 5, len(xs)))

def plot_widths(xs, ys, widths, ax=None, color='b', xlim=None, ylim=None,
                **kwargs):
    if not (len(xs) == len(ys) == len(widths)):
        raise ValueError('xs, ys, and widths must have identical lengths')
    fig = None
    if ax is None:
        fig, ax = plt.subplots(1)

    segmentx, segmenty = [xs[0]], [ys[0]]
    current_width = widths[0]
    for ii, (x, y, width) in enumerate(zip(xs, ys, widths)):
        segmentx.append(x)
        segmenty.append(y)
        if (width != current_width) or (ii == (len(xs) - 1)):
            ax.plot(segmentx, segmenty, linewidth=current_width, color=color,
                    **kwargs)
            segmentx, segmenty = [x], [y]
            current_width = width
    if xlim is None:
        xlim = [min(xs), max(xs)]
    if ylim is None:
        ylim = [min(ys), max(ys)]
    ax.set_xlim(xlim)
    ax.set_ylim(ylim)

    return ax if fig is None else fig

plot_widths(xs, ys, widths)
plt.show()
Adjudge answered 31/5, 2015 at 23:57 Comment(1)
This implementation may be good in some cases, however, if you try it on the sinusoidal example given by gg349 it suffers. Both doesn't look as good and is quite slow since it adds a new line object for each segment because the width is continuously changing.Kellie

© 2022 - 2024 — McMap. All rights reserved.