10

Similarly to a previous question of mine, I'd like to control the capstyle of lines being drawn using matplotlib. However, I have an extremely large number of lines, and drawing with anything other than a line collection takes way too long. Are there any workarounds to control the capstyle of lines in a line collection in a generic way (or alternatively, super fast ways of drawing a large number of Line2D lines). For instance, I've tried using the matplotlib rc settings via:

import matplotlib as mpl
mpl.rcParams['lines.solid_capstyle'] = 'round'
mpl.rcParams['lines.solid_joinstyle'] = 'round'

But this doesn't appear to have any affect. From the docstring for collections.py:

The classes are not meant to be as flexible as their single element counterparts (e.g. you may not be able to select all line styles) but they are meant to be fast for common use cases (e.g. a large set of solid line segemnts)

Which explains why I can't seem to control various parameters, but I still want to do it! I've had a look at the code for the AGG backend (_backend_agg.cpp: not that I really understand it), and it appears that line_cap and line_join are controlled by gc.cap and gc.join, where gc comes from the GCAgg class. Does anyone know how one can control this from Python? Am I asking the right question here? Perhaps that are easier ways to control these parameters?

Any help is greatly appreciated... I'm desperate to get this working, so even crazy hacks are welcome!

Thanks,

Carson

Community
  • 1
  • 1
Carson Farmer
  • 506
  • 3
  • 19

3 Answers3

6

Since you mention in your question that you don't mind "dirty" solutions, one option would as follows.

The "drawing process" of a particular LineCollection is handled by the draw method defined in the Collection class (the base of LineCollection). This method creates an instance of GraphicsContextBase (defined in backend_bases.py) via the statement gc = renderer.new_gc(). It seems to be exactly this object which governs among other things the properties controlling the capstyle (property _capstyle). Therefore, one could subclass GraphicsContextBase, override the _capstyle property, and inject a new new_gc method into the RendererBase class so that consequent calls to new_gc return the customized instance:

Borrowing the example from the answer by @florisvb (assuming Python3):

#!/usr/bin/env python
import types

import numpy as np
from matplotlib.backend_bases import GraphicsContextBase, RendererBase
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection

class GC(GraphicsContextBase):
    def __init__(self):
        super().__init__()
        self._capstyle = 'round'

def custom_new_gc(self):
    return GC()

RendererBase.new_gc = types.MethodType(custom_new_gc, RendererBase)
#----------------------------------------------------------------------
np.random.seed(42)

x = np.random.random(10)
y = np.random.random(10)

points = np.array([x, y]).T.reshape((-1, 1, 2))
segments = np.concatenate([points[:-1], points[1:]], axis=1)

fig = plt.figure()
ax = fig.add_subplot(111)

linewidth = 10
lc = LineCollection(segments, linewidths=linewidth)
ax.add_collection(lc)

fig.savefig('fig.png')

This produces: enter image description here

ewcz
  • 12,819
  • 1
  • 25
  • 47
  • Nice. That looks gorgeous! Thanks @ewcz! – Carson Farmer May 14 '17 at 19:50
  • @ewcz This solution works great for agg based output, thanks! I've been looking through the source for other backends because I'd like to get this effect in a pdf too. I can't seem to get the 'ps' or the 'pdf' backends to accept the patch that you've written, but interestingly the 'svg' backend can handle it. Any thoughts on how to tweak this patch for pdf output? – aorr Jul 09 '17 at 16:42
  • Thank you for the answer, but export to pdf does not work, I used this answer to make it work https://stackoverflow.com/questions/49983192/how-to-plot-and-export-multi-colored-line-with-smooth-edges?noredirect=1&lq=1 – BaptisteL Dec 07 '20 at 09:45
4

To update the answer from @ewcz as this thread still comes up in search results.
One can now use path_effects instead of defining their own GraphicsContextBase.

e.g.

import numpy as np
import matplotlib.patheffects as path_effects
from matplotlib.collections import LineCollection

np.random.seed(42)

x = np.random.random(10)
y = np.random.random(10)

points = np.array([x, y]).T.reshape((-1, 1, 2))
segments = np.concatenate([points[:-1], points[1:]], axis=1)

fig = plt.figure()
ax = fig.add_subplot(111)

linewidth = 10

### Stroke redraws the segment passing kwargs down to the GC renderer
lc = LineCollection(segments, linewidths=linewidth, 
    path_effects=[path_effects.Stroke(capstyle="round")])

ax.add_collection(lc)

fig.show()

Example png output with smooth lines and it also seems to work well with pdf output

beez
  • 43
  • 4
1

I was struggling with the same issue. I ended up plotting a scatter plot on top of my line collection. It's not perfect, but it may work for your application. There's a few subtleties - below is a working example.

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection

x = np.random.random(10)
y = np.random.random(10)
z = np.arange(0,10)

points = np.array([x, y]).T.reshape(-1, 1, 2)
segments = np.concatenate([points[:-1], points[1:]], axis=1)

fig = plt.figure()
ax = fig.add_subplot(111)

linewidth = 10
cmap = plt.get_cmap('jet')
norm = plt.Normalize(np.min(z), np.max(z))
color = cmap(norm(z))

lc = LineCollection(segments, linewidths=linewidth, cmap=cmap, norm=norm)
lc.set_array(z)
lc.set_zorder(z.tolist())
ax.add_collection(lc)

ax.scatter(x,y,color=color,s=linewidth**2,edgecolor='none', zorder=(z+2).tolist())
florisvb
  • 61
  • 5