Custom markers with screen coordinate size in (older) matplotlib
Question: Originally, I was hoping that I could create a sort of a class for a marker in
`matplotlib`, which would be a square with text showing the, say, x coordinate
and a label, so I could instantiate it with something like (pseudocode):

    
    
    plt.plot(..., marker=myMarkerClass(label="X:"), ... )
    

... but as far as I can see, you cannot do stuff like that.

However, it seems that customization of markers is not available in older
`matplotlib`; so I'd like to reduce my question to: how to get custom (path)
markers in older `matplotlib`, so their sizes are defined in screen
coordinates (so markers don't scale upon zoom)? To clarify, here are some
examples:

## Default (uncustomized) markers

Below is an example with the default `matplotlib` markers, which works with
older `matplotlib`. Note that I've tried instead of using `pyplot.plot()`, I'm
trying to work with `matplotlib.figure.Figure` directly (as that is the form
usually used with diverse backends), requiring use of "figure manager" (see
also [matplotlib-devel - Backends object
structure](http://matplotlib.1069221.n5.nabble.com/Backends-object-structure-
td29565.html)):

    
    
    import matplotlib
    import matplotlib.pyplot as plt
    import matplotlib.figure
    import numpy as np
    
    t = np.arange(0.0,1.5,0.25)
    s = np.sin(2*np.pi*t)
    
    mfigure = matplotlib.figure.Figure(figsize=(5,4), dpi=100)
    ax = mfigure.add_subplot(111)
    ax.plot(t,s, marker='o', color='b', markerfacecolor='orange', markersize=10.0)
    
    fig = plt.figure() # create something (fig num 1) for fig_manager
    figman = matplotlib._pylab_helpers.Gcf.get_fig_manager(1)
    figman.canvas.figure = mfigure    # needed
    mfigure.set_canvas(figman.canvas) # needed
    plt.show()
    

If we do an arbitrary zoom rect here, the markers remain the same size:

![markers-ex01](http://i.stack.imgur.com/13LYQ.png)

## Customized markers via Path

This is documented in [artists (Line2D.set_marker) — Matplotlib 1.2.1
documentation](http://matplotlib.org/api/artist_api.html#matplotlib.lines.Line2D.set_marker);
however, it doesn't work with older `matplotlib`; here's an example:

    
    
    import matplotlib
    import matplotlib.pyplot as plt
    import matplotlib.figure
    import matplotlib.path
    import numpy as np
    print("matplotlib version {0}".format(matplotlib.__version__))
    
    def getCustomSymbol1(inx, iny, sc, yasp):
      verts = [
          (-0.5, -0.5), # left, bottom
          (-0.5, 0.5), # left, top
          (0.5, 0.5), # right, top
          (0.5, -0.5), # right, bottom
          (-0.5, -0.5), # ignored
          ]
      codes = [matplotlib.path.Path.MOVETO,
               matplotlib.path.Path.LINETO,
               matplotlib.path.Path.LINETO,
               matplotlib.path.Path.LINETO,
               matplotlib.path.Path.CLOSEPOLY,
               ]
      pathCS1 = matplotlib.path.Path(verts, codes)
      return pathCS1, verts
    
    t = np.arange(0.0,1.5,0.25)
    s = np.sin(2*np.pi*t)
    
    mfigure = matplotlib.figure.Figure(figsize=(5,4), dpi=100)
    ax = mfigure.add_subplot(111)
    pthCS1, vrtCS1 = getCustomSymbol1(0,0, 1,1)
    # here either marker=pthCS1 or marker=np.array(vrtCS1)
    # have the same effect:
    ax.plot(t,s, marker=pthCS1, markerfacecolor='orange', markersize=10.0)
    #ax.plot(t,s, marker=np.array(vrtCS1), markerfacecolor='orange', markersize=10.0)
    
    fig = plt.figure() # create something (fig num 1) for fig_manager
    figman = matplotlib._pylab_helpers.Gcf.get_fig_manager(1)
    figman.canvas.figure = mfigure    # needed
    mfigure.set_canvas(figman.canvas) # needed
    plt.show()
    

This runs fine for me in newer `matplotlib`, but fails in the older:

    
    
    $ python3.2 test.py 
    matplotlib version 1.2.0
    
    $ python2.7 test.py      # marker=pthCS1
    matplotlib version 0.99.3
    Traceback (most recent call last):
      File "test.py", line 36, in <module>
        ax.plot(t,s, marker=pthCS1, markerfacecolor='orange', markersize=10.0)
      ...
      File "/usr/lib/pymodules/python2.7/matplotlib/lines.py", line 804, in set_marker
        self._markerFunc = self._markers[marker]
    KeyError: Path([[-0.5 -0.5]
     [-0.5  0.5]
     [ 0.5  0.5]
     [ 0.5 -0.5]
     [-0.5 -0.5]], [ 1  2  2  2 79])
    
    $ python2.7 test.py      # marker=np.array(vrtCS1)
    matplotlib version 0.99.3
    Traceback (most recent call last):
      File "test.py", line 38, in <module>
        ax.plot(t,s, marker=np.array(vrtCS1), markerfacecolor='orange', markersize=10.0)
      ...
      File "/usr/lib/pymodules/python2.7/matplotlib/lines.py", line 798, in set_marker
        if marker not in self._markers:
    TypeError: unhashable type: 'numpy.ndarray'
    

However, when it works in Python 3.2, the markers again keep their size across
zoom of the graph, as I'd expect:

![markers-ex02](http://i.stack.imgur.com/FAWu5.png)

... though note this issue: [Custom marker created from vertex list scales
wrong · Issue #1980 · matplotlib/matplotlib ·
GitHub](https://github.com/matplotlib/matplotlib/issues/1980), in respect to
this type of custom markers.

## Through Paths in PatchCollection

I've picked up parts of this code from some internet postings, but cannot find
the links now. In any case, we can avoid drawing the markers and
PatchCollection can be used, to draw what should be the markers. Here is the
code, which runs in older `matplotlib`:

    
    
    import matplotlib
    import matplotlib.pyplot as plt
    import matplotlib.figure
    import matplotlib.path, matplotlib.patches, matplotlib.collections
    import numpy as np
    
    def getCustomSymbol1(inx, iny, sc, yasp):
      verts = [
          (inx-0.5*sc, iny-0.5*sc*yasp), # (0., 0.), # left, bottom
          (inx-0.5*sc, iny+0.5*sc*yasp), # (0., 1.), # left, top
          (inx+0.5*sc, iny+0.5*sc*yasp), # (1., 1.), # right, top
          (inx+0.5*sc, iny-0.5*sc*yasp), # (1., 0.), # right, bottom
          (inx-0.5*sc, iny-0.5*sc*yasp), # (0., 0.), # ignored
          ]
      codes = [matplotlib.path.Path.MOVETO,
               matplotlib.path.Path.LINETO,
               matplotlib.path.Path.LINETO,
               matplotlib.path.Path.LINETO,
               matplotlib.path.Path.CLOSEPOLY,
               ]
      pathCS1 = matplotlib.path.Path(verts, codes)
      return pathCS1
    def getXyIter(inarr):
      # this supports older numpy, where nditer is not available
      if np.__version__ >= "1.6.0":
        return np.nditer(inarr.tolist())
      else:
        dimensions = inarr.shape
        xlen = dimensions[1]
        xinds = np.arange(0, xlen, 1)
        return np.transpose(np.take(inarr, xinds, axis=1))
    
    t = np.arange(0.0,1.5,0.25)
    s = np.sin(2*np.pi*t)
    
    mfigure = matplotlib.figure.Figure(figsize=(5,4), dpi=100)
    ax = mfigure.add_subplot(111)
    ax.plot(t,s)
    
    customMarkers=[]
    for x, y in getXyIter(np.array([t,s])): #np.nditer([t,s]):
      #printse("%f:%f\n" % (x,y))
      pathCS1 = getCustomSymbol1(x,y,0.05,1.5*500.0/400.0)
      patchCS1 = matplotlib.patches.PathPatch(pathCS1, facecolor='orange', lw=1) # no
      customMarkers.append(patchCS1)
    pcolm = matplotlib.collections.PatchCollection(customMarkers)
    pcolm.set_alpha(0.9)
    ax.add_collection(pcolm)
    
    fig = plt.figure() # create something (fig num 1) for fig_manager
    figman = matplotlib._pylab_helpers.Gcf.get_fig_manager(1)
    figman.canvas.figure = mfigure    # needed
    mfigure.set_canvas(figman.canvas) # needed
    plt.show()
    

Now, here I tried to take figure initial aspect ratio into consideration and
indeed, at first render, the "markers" look right in respect to size - but...:

![markers-ex03](http://i.stack.imgur.com/Qe6lr.png)

... when we try to do arbitrary zoom, it is obvious that the paths have been
specified in data coordinates, and so their sizes change depending on the zoom
rect. (Another nuissance is that `facecolor='orange'` is not obeyed; but that
can be fixed with `pcolm.set_facecolor('orange')`)



So, is there a way that I can use PatchCollection as markers for older
`matplotlib`, in the sense that the rendered paths would be defined in screen
coordinates, so they would not change their size upon arbitrary zooming?


Answer: Many thanks to @tcaswell for the
[comment](http://stackoverflow.com/questions/16649970/custom-markers-with-
screen-coordinate-size-in-older-matplotlib#comment23952132_16649970) on
[blended
transform](http://matplotlib.org/users/transforms_tutorial.html#blended-
transformations) \- in trying to figure out why _that_ doesn't work for me, I
finally found the solution. First, the code - using, essentially, the default
marker engine of matplotlib (depending on whether using older matplotlib
(0.99) or newer one):

    
    
    import matplotlib
    import matplotlib.pyplot as plt
    import matplotlib.figure
    import numpy as np
    
    # create vertices and Path of custom symbol
    def getCustomSymbol1():
      verts = [
          (0.0, 0.0), # left, bottom
          (0.0, 0.7), # left, top
          (1.0, 1.0), # right, top
          (0.8, 0.0), # right, bottom
          (0.0, 0.0), # ignored
          ]
      codes = [matplotlib.path.Path.MOVETO,
               matplotlib.path.Path.LINETO,
               matplotlib.path.Path.LINETO,
               matplotlib.path.Path.LINETO,
               matplotlib.path.Path.CLOSEPOLY,
               ]
      pathCS1 = matplotlib.path.Path(verts, codes)
      return pathCS1, verts
    
    if matplotlib.__version__ < "1.0.0":
      # define a marker drawing function, that uses
      # the above custom symbol Path
      def _draw_mypath(self, renderer, gc, path, path_trans):
        gc.set_snap(renderer.points_to_pixels(self._markersize) >= 2.0)
        side = renderer.points_to_pixels(self._markersize)
        transform = matplotlib.transforms.Affine2D().translate(-0.5, -0.5).scale(side)
        rgbFace = self._get_rgb_face()
        mypath, myverts = getCustomSymbol1()
        renderer.draw_markers(gc, mypath, transform,
                              path, path_trans, rgbFace)
      # add this function to the class prototype of Line2D
      matplotlib.lines.Line2D._draw_mypath = _draw_mypath
      # add marker shortcut/name/command/format spec '@' to Line2D class,
      # and relate it to our custom marker drawing function
      matplotlib.lines.Line2D._markers['@'] = '_draw_mypath'
      matplotlib.lines.Line2D.markers = matplotlib.lines.Line2D._markers
    else:
      import matplotlib.markers
      def _set_mypath(self):
        self._transform = matplotlib.transforms.Affine2D().translate(-0.5, -0.5)
        self._snap_threshold = 2.0
        mypath, myverts = getCustomSymbol1()
        self._path = mypath
        self._joinstyle = 'miter'
      matplotlib.markers.MarkerStyle._set_mypath = _set_mypath
      matplotlib.markers.MarkerStyle.markers['@'] = 'mypath'
      matplotlib.lines.Line2D.markers = matplotlib.markers.MarkerStyle.markers
    
    # proceed as usual - use the new marker '@'
    t = np.arange(0.0,1.5,0.25)
    s = np.sin(2*np.pi*t)
    
    mfigure = matplotlib.figure.Figure(figsize=(5,4), dpi=100)
    ax = mfigure.add_subplot(111)
    ax.plot(t,s, marker='@', color='b', markerfacecolor='orange', markersize=20.0)
    
    fig = plt.figure() # create something (fig num 1) for fig_manager
    figman = matplotlib._pylab_helpers.Gcf.get_fig_manager(1)
    figman.canvas.figure = mfigure    # needed
    mfigure.set_canvas(figman.canvas) # needed
    plt.show()
    

It shows a slightly "creative" marker (if I may say so myself `:)` ) - and
this is how it behaves under zoom:

![custom-marker-fix](http://i.stack.imgur.com/7X7JV.png)

... that is, exactly as I want it - markers keep their position in data
coordinates; however preserve their size under zooming.

* * *

### Discussion

One of the most confusing things for me here, was the following: Essentially,
you can specify a rectangle through an x,y coordinate as "location", and size
(height/width). So if I specify a rectangle at x,y=(2,3); with halfsize 2 (so,
square); then I can calculate its path _as vertices_ through:

    
    
    [(x-hs,y-hs), (x-hs,y+hs), (x+hs,y+hs), (x+hs,y-hs)]
    

This is, essentially, what `getCustomSymbol1` was trying to return all along.
In addition, for instance `matplotlib.patches.Rectangle` is instantiated
through location and size, as in `Rectangle((x,y), width, height)`.

Now, the problem is - what I actually wanted, is that markers, as shapes,
_stay_ on their position - in data coordinates, so moving and zooming the
graph keeps their position _as data_ ; however, they should have kept their
size under zooming.

This means that I want `(x,y)` specified in one coordinate system (data), and
`size` (or `width`, `height`, or `halfsize`) specified in _another_ coordinate
system, in this case, the screen coordinate system: since I want the shapes to
keep their size under zoom, I actually want to keep their size _in screen
pixels_ the same!

That is why _no transformation_ as such would help in my case - any
transformation would work on _all_ the vertices of a path, as interpreted in a
single coordinate system! Whereas, what I want, would be to get something
like:

    
    
    hsd = screen2dataTransform(10px)
    [(x-hsd,y-hsd), (x-hsd,y+hsd), (x+hsd,y+hsd), (x+hsd,y-hsd)]
    

... and this recalculation of the vertices of the marker path would have to be
repeated each time the zoom level, or the figure pan, or the window size
changes.

As such, vertices/paths (and patches) and transformation alone cannot be used
for this purpose. However, thankfully, we can use `matplotlib`s own engine; we
simply have to know that any call to `ax.plot(...marker=...)` actually
delegates the drawing of markers to `matplotlib.lines.Line2D`; and `Line2D`
maintains an internal dict, which relates the marker one-character format
specifier/command (like `'o'`, `'*'` etc) to a specific drawing function; and
the drawing function, finally, does the size transformation in code like (in
the solution code above, the drawing is for the most part taken from the `'s'`
(square) marker implementation):

    
    
    side = renderer.points_to_pixels(self._markersize)
    transform = matplotlib.transforms.Affine2D().translate(-0.5, -0.5).scale(side)
    

Note that this is the case for older `matplotlib` (0.99.3 in my case); for
newer `matplotlib` (1.2.0 in my case), there is a separate class `MarkerStyle`
which maintains the relationship between the marker format specifier, and the
function - and the function is not `_draw_` anymore, it is just `_set_` \- but
other than that, it is the same principle.

NB: I'm actually not sure when `MarkerStyle` was introduced, I could only find
[matplotlib.markers — Matplotlib 1.3.x
documentation](http://cbio.ensmp.fr/~nvaroquaux/matplotlib/doc/api/markers_api.html)
and it doesn't say; so that `if matplotlib.__version__ < "1.0.0":` in the code
above could be wrong; but worksforme (fornow).

Because the markers size is, so to speak, managed separately (from its
position) - that means that you don't have to do any special calculation in
your custom marker Path specification; all you need to do is make sure its
vertices fit in the range (0.0,0.0) to (1.0, 1.0) - the marker drawing engine
will do the rest.

Well, I hope I understood this right - but if there are other approaches that
work like this, I'd sure like to know about those :)

Hope this helps someone,  
Cheers!

