Expand the line with specified width in data unit

The following code is a generic example on how to make a line plot in matplotlib using data coordinates as linewidth. There are two solutions; one using callbacks, one using subclassing Line2D.

Using callbacks.

It is implemted as a class data_linewidth_plot that can be called with a signature pretty close the the normal plt.plot command,

l = data_linewidth_plot(x, y, ax=ax, label="some line", linewidth=1, alpha=0.4)

where ax is the axes to plot to. The ax argument can be omitted, when only one subplot exists in the figure. The linewidth argument is interpreted in (y-)data units.

Further features:

  1. It’s independend on the subplot placements, margins or figure size.
  2. If the aspect ratio is unequal, it uses y data coordinates as the linewidth.
  3. It also takes care that the legend handle is correctly set (we may want to have a huge line in the plot, but certainly not in the legend).
  4. It is compatible with changes to the figure size, zoom or pan events, as it takes care of resizing the linewidth on such events.

Here is the complete code.

import matplotlib.pyplot as plt

class data_linewidth_plot():
    def __init__(self, x, y, **kwargs):
        self.ax = kwargs.pop("ax", plt.gca())
        self.fig = self.ax.get_figure()
        self.lw_data = kwargs.pop("linewidth", 1)
        self.lw = 1
        self.fig.canvas.draw()

        self.ppd = 72./self.fig.dpi
        self.trans = self.ax.transData.transform
        self.linehandle, = self.ax.plot([],[],**kwargs)
        if "label" in kwargs: kwargs.pop("label")
        self.line, = self.ax.plot(x, y, **kwargs)
        self.line.set_color(self.linehandle.get_color())
        self._resize()
        self.cid = self.fig.canvas.mpl_connect('draw_event', self._resize)

    def _resize(self, event=None):
        lw =  ((self.trans((1, self.lw_data))-self.trans((0, 0)))*self.ppd)[1]
        if lw != self.lw:
            self.line.set_linewidth(lw)
            self.lw = lw
            self._redraw_later()

    def _redraw_later(self):
        self.timer = self.fig.canvas.new_timer(interval=10)
        self.timer.single_shot = True
        self.timer.add_callback(lambda : self.fig.canvas.draw_idle())
        self.timer.start()

fig1, ax1 = plt.subplots()
#ax.set_aspect('equal') #<-not necessary 
ax1.set_ylim(0,3)
x = [0,1,2,3]
y = [1,1,2,2]

# plot a line, with 'linewidth' in (y-)data coordinates.       
l = data_linewidth_plot(x, y, ax=ax1, label="some 1 data unit wide line", 
                        linewidth=1, alpha=0.4)

plt.legend() # <- legend possible
plt.show()

enter image description here

(I updated the code to use a timer to redraw the canvas, due to this issue)

Subclassing Line2D

The above solution has some drawbacks. It requires a timer and callbacks to update itself on changing axis limits or figure size. The following is a solution without such needs. It will use a dynamic property to always calculate the linewidth in points from the desired linewidth in data coordinates on the fly. It is much shorter than the above.
A drawback here is that a legend needs to be created manually via a proxyartist.

import matplotlib.pyplot as plt
from matplotlib.lines import Line2D

class LineDataUnits(Line2D):
    def __init__(self, *args, **kwargs):
        _lw_data = kwargs.pop("linewidth", 1) 
        super().__init__(*args, **kwargs)
        self._lw_data = _lw_data

    def _get_lw(self):
        if self.axes is not None:
            ppd = 72./self.axes.figure.dpi
            trans = self.axes.transData.transform
            return ((trans((1, self._lw_data))-trans((0, 0)))*ppd)[1]
        else:
            return 1

    def _set_lw(self, lw):
        self._lw_data = lw

    _linewidth = property(_get_lw, _set_lw)


fig, ax = plt.subplots()

#ax.set_aspect('equal') # <-not necessary, if not given, y data is assumed 
ax.set_xlim(0,3)
ax.set_ylim(0,3)
x = [0,1,2,3]
y = [1,1,2,2]

line = LineDataUnits(x, y, linewidth=1, alpha=0.4)
ax.add_line(line)

ax.legend([Line2D([],[], linewidth=3, alpha=0.4)], 
           ['some 1 data unit wide line'])    # <- legend possible via proxy artist
plt.show()

Leave a Comment