Take the 2-minute tour ×

Is it possible to tie the linewidth of a matplotlib path to the figure zoom/scale level?

I am drawing a map where the matplotlib path (with bezier curves) draws the road on the map. Upon zooming in I would like the width of the path to zoom in.

In attached script, the polygonal approximation can properly zoom, but the path (red line) cannot zoom (in width).

Is it possible to tie the linewidth to some scale transformation and redraw via callback ?

import matplotlib.pyplot as plt
from matplotlib.path import Path
import matplotlib.patches as patches
import numpy as np

def main():
  ax = plt.subplot(111)
  verts = np.array([ (0., 0.), (0.5, .5), (1., 0.8), (0.8, 0.)])
  codes = np.array([Path.MOVETO, Path.CURVE4, Path.CURVE4, Path.LINETO ])

  # Can this curve have zoomable width
  path = Path(verts, codes)
  patch = patches.PathPatch(path, fc='none', color='r', lw=4, zorder=3)

  ax.plot(verts[:,0], verts[:,1], 'o--', lw=2, color='k', zorder=2)

  # these will be polygonal approx that will have proper zoom
  for i in range(len(verts)-1):
    vtmp, ctmp = line2poly(verts[[i,i+1],:],0.03)
    v = np.vstack( (v,vtmp) )
    c = np.concatenate( (c,ctmp) )
  path_zoom = Path(v,c)
  patch_zoom =  patches.PathPatch(path_zoom, fc='r', ec='k', zorder=1, alpha=0.4)

  ax.set_xlim(-0.1, 1.1)
  ax.set_ylim(-0.1, 1.1)

def line2poly(line, width):
  dx,dy = np.hstack(np.diff(line,axis=0)).tolist()
  theta = np.arctan2(dy,dx)
  s = width/2 * np.sin(theta)
  c = width/2 * np.cos(theta)
  trans = np.array([(-s,c),(s,-c),(s,-c),(-s,c)])

  verts = line[[0,0,1,1],:]+trans
  verts = np.vstack((verts, verts[0,:]))
  codes = np.array([Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY])
  return verts,codes

if __name__=='__main__':
share|improve this question
plt.quiver does this somehow, but I'm not sure how to utilize that information :P –  askewchan Mar 27 '13 at 23:18
hmmm.... plt.quiver does not do this for me. –  waqy Mar 28 '13 at 0:50
@askewchan - quiver does it because it uses polygons instead of paths (i.e. there's no stroke for the arrows, instead they're polygons). –  Joe Kington Mar 28 '13 at 2:18
Ah, thanks @Joe, that makes sense. –  askewchan Mar 28 '13 at 4:10
add comment

1 Answer

up vote 4 down vote accepted

To the best of my knowledge, there's no way to do this in matplotlib, as the stroke width of a line cannot be directly tied to data coordinates. (As you mentioned, you could connect a callback to the draw event and accomplish this. It would incur a large performance penalty, though.)

However, a quick workaround would be to use shapely to generate polygons by buffering your street paths.

As a quick example:

import shapely.geometry
import descartes
import matplotlib.pyplot as plt

lines = ([(0, 0), (1, 0), (0, 1)],
         [(0, 0), (1, 1)],
         [(0.5, 0.5), (1, 0.5)],
lines = shapely.geometry.MultiLineString(lines)
# "0.05" is the _radius_ in data coords, so the width will be 0.1 units.
poly = lines.buffer(0.05)

fig, ax = plt.subplots()
patch = descartes.PolygonPatch(poly, fc='gray', ec='black')

# Rescale things to leave a bit of room around the edges...


enter image description here

If you did want to take the callback route, you might do something like this:

import matplotlib.pyplot as plt

def main():

    fig, ax = plt.subplots()
    artists = []
    for verts in lines:
        x, y = zip(*verts)
        line, = ax.plot(x, y)

    scalar = StrokeScalar(artists, 0.1)
    ax.callbacks.connect('xlim_changed', scalar)
    ax.callbacks.connect('ylim_changed', scalar)



class StrokeScalar(object):
    def __init__(self, artists, width):
        self.width = width
        self.artists = artists
        # Assume there's only one axes and one figure, for the moment...
        self.ax = artists[0].axes
        self.fig = self.ax.figure

    def __call__(self, event):
        """Intended to be connected to a draw event callback."""
        for artist in self.artists:

    def stroke_width(self):
        positions = [[0, 0], [self.width, self.width]]
        to_inches = self.fig.dpi_scale_trans.inverted().transform
        pixels = self.ax.transData.transform(positions)
        points = to_inches(pixels) * 72
        return points.ptp(axis=0).mean() # Not quite correct...


enter image description here

share|improve this answer
Is it possible to draw bezier curves in shapely and dilate/buffer them? –  waqy Mar 28 '13 at 3:47
@waqy - No, shapely treats lines as linear segments. You'd have to approximate them. I updated my answer with a callback-based way to do it using only matplotlib (it's exactly correct unless the aspect ratio is set to 1). –  Joe Kington Mar 28 '13 at 3:49
Thanks. Your callback-based scheme works perfectly (for curves and text too.) –  waqy Mar 28 '13 at 20:47
Glad it helped! There are some oddities with it in certain cases that I can't quite figure out. Also, in my previous comment, I meant to say "it's not exactly correct unless the aspect ratio is set to 1". On a brighter note, the "large performance penalty" I mentioned turns out to be completely insignificant. That's what I get for guessing before I try it! –  Joe Kington Mar 29 '13 at 2:03
add comment

Your Answer


