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.

4 Answers 4

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

Sign up to request clarification or add additional context in comments.

5 Comments

Nice! So you cut the line into a series of pieces and use LineCollection to specify the properties in each piece.
Not at all what I was looking for, but this is pretty cool, so I up voted :)
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...
I would suggest to post a new question, linking to this question as a reference
Coming to this much later, a hugely helpful answer!. To reply to the comment from @dwanderson 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 them
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.

1 Comment

Regarding your problem (1) and maybe your problem (2) with Gulio's answer, I found a workaround passing the argument antialiaseds=False to LineCollection .
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()

1 Comment

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.
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()

1 Comment

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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.