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):
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:
Customized markers via Path
This is documented in artists (Line2D.set_marker) — Matplotlib 1.2.1 documentation; 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:
... though note this issue: Custom marker created from vertex list scales wrong · Issue #1980 · matplotlib/matplotlib · GitHub, 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...:
... 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?