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.

Hamid
  • 1,355
  • 1
  • 11
  • 21

4 Answers4

92

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

gg349
  • 21,996
  • 5
  • 54
  • 64
  • 1
    Nice! So you cut the line into a series of pieces and use `LineCollection` to specify the properties in each piece. – Hamid Jan 25 '14 at 18:05
  • 9
    Not at all what I was looking for, but this is pretty cool, so I up voted :) – tommy.carstensen Jul 07 '15 at 15:55
  • 1
    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... – dwanderson Oct 05 '16 at 15:27
  • I would suggest to post a new question, linking to this question as a reference – gg349 Oct 09 '16 at 01:40
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.

J.Bet
  • 101
  • 1
  • 3
  • Regarding your problem (1) and maybe your problem (2) with Gulio's answer, I found a workaround passing the argument antialiaseds=False to LineCollection . – Rogério Gouvêa Oct 08 '21 at 03:31
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()
piokuc
  • 25,594
  • 11
  • 72
  • 102
  • 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. – Hamid Oct 16 '13 at 01:58
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()
kingjr
  • 173
  • 1
  • 7
  • 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. – Hamid Jun 02 '15 at 01:48