Repository URL to install this package:
|
Version:
1.1.0 ▾
|
matplotlib
/
markers.py
|
|---|
"""
This module contains functions to handle markers. Used by both the
marker functionality of `~matplotlib.axes.Axes.plot` and
`~matplotlib.axes.Axes.scatter`.
"""
import textwrap
import numpy as np
from cbook import is_math_text, is_string_like, is_numlike, iterable
import docstring
from matplotlib import rcParams
from path import Path
from transforms import IdentityTransform, Affine2D
# special-purpose marker identifiers:
(TICKLEFT, TICKRIGHT, TICKUP, TICKDOWN,
CARETLEFT, CARETRIGHT, CARETUP, CARETDOWN) = range(8)
class MarkerStyle:
style_table = """
============================== ===============================================
marker description
============================== ===============================================
%s
``'$...$'`` render the string using mathtext
*verts* a list of (x, y) pairs in range (0, 1)
(*numsides*, *style*, *angle*) see below
============================== ===============================================
The marker can also be a tuple (*numsides*, *style*, *angle*), which
will create a custom, regular symbol.
*numsides*:
the number of sides
*style*:
the style of the regular symbol:
===== =============================================
Value Description
===== =============================================
0 a regular polygon
1 a star-like symbol
2 an asterisk
3 a circle (*numsides* and *angle* is ignored)
===== =============================================
*angle*:
the angle of rotation of the symbol
For backward compatibility, the form (*verts*, 0) is also accepted,
but it is equivalent to just *verts* for giving a raw set of vertices
that define the shape.
"""
# TODO: Automatically generate this
accepts = """ACCEPTS: [ %s | ``'$...$'`` | *tuple* | *Nx2 array* ]"""
markers = {
'.' : 'point',
',' : 'pixel',
'o' : 'circle',
'v' : 'triangle_down',
'^' : 'triangle_up',
'<' : 'triangle_left',
'>' : 'triangle_right',
'1' : 'tri_down',
'2' : 'tri_up',
'3' : 'tri_left',
'4' : 'tri_right',
'8' : 'octagon',
's' : 'square',
'p' : 'pentagon',
'*' : 'star',
'h' : 'hexagon1',
'H' : 'hexagon2',
'+' : 'plus',
'x' : 'x',
'D' : 'diamond',
'd' : 'thin_diamond',
'|' : 'vline',
'_' : 'hline',
TICKLEFT : 'tickleft',
TICKRIGHT : 'tickright',
TICKUP : 'tickup',
TICKDOWN : 'tickdown',
CARETLEFT : 'caretleft',
CARETRIGHT : 'caretright',
CARETUP : 'caretup',
CARETDOWN : 'caretdown',
"None" : 'nothing',
None : 'nothing',
' ' : 'nothing',
'' : 'nothing'
}
# Just used for informational purposes. is_filled()
# is calculated in the _set_* functions.
filled_markers = (
'o', 'v', '^', '<', '>', '8', 's', 'p', '*', 'h', 'H', 'D', 'd')
fillstyles = ('full', 'left' , 'right' , 'bottom' , 'top')
# TODO: Is this ever used as a non-constant?
_point_size_reduction = 0.5
def __init__(self, marker=None, fillstyle='full'):
self._fillstyle = fillstyle
self.set_marker(marker)
self.set_fillstyle(fillstyle)
def _recache(self):
self._path = Path(np.empty((0,2)))
self._transform = IdentityTransform()
self._alt_path = None
self._alt_transform = None
self._snap_threshold = None
self._filled = True
self._marker_function()
def __nonzero__(self):
return len(self._path.vertices)
def is_filled(self):
return self._filled
def get_fillstyle(self):
return self._fillstyle
def set_fillstyle(self, fillstyle):
# TODO: Raise exception for markers where fillstyle doesn't make sense
assert fillstyle in self.fillstyles
self._fillstyle = fillstyle
self._recache()
def get_marker(self):
return self._marker
def set_marker(self, marker):
if (iterable(marker) and len(marker) in (2, 3) and
marker[1] in (0, 1, 2, 3)):
self._marker_function = self._set_tuple_marker
elif marker in self.markers:
self._marker_function = getattr(
self, '_set_' + self.markers[marker])
elif is_string_like(marker) and is_math_text(marker):
self._marker_function = self._set_mathtext_path
elif isinstance(marker, Path):
self._marker_function = self._set_path_marker
else:
try:
path = Path(marker)
self._marker_function = self._set_vertices
except:
raise ValueError('Unrecognized marker style %s' % marker)
self._marker = marker
self._recache()
def get_path(self):
return self._path
def get_transform(self):
return self._transform.frozen()
def get_alt_path(self):
return self._alt_path
def get_alt_transform(self):
return self._alt_transform.frozen()
def get_snap_threshold(self):
return self._snap_threshold
def _set_nothing(self):
self._filled = False
def _set_custom_marker(self, path):
verts = path.vertices
rescale = max(np.max(np.abs(verts[:,0])), np.max(np.abs(verts[:,1])))
self._transform = Affine2D().scale(1.0 / rescale)
self._path = path
def _set_path_marker(self):
self._set_custom_marker(self._marker)
def _set_vertices(self):
path = Path(verts)
self._set_custom_marker(path)
def _set_tuple_marker(self):
marker = self._marker
if is_numlike(marker[0]):
if len(marker) == 2:
numsides, rotation = marker[0], 0.0
elif len(marker) == 3:
numsides, rotation = marker[0], marker[2]
symstyle = marker[1]
if symstyle == 0:
self._path = Path.unit_regular_polygon(numsides)
elif symstyle == 1:
self._path = Path.unit_regular_star(numsides)
elif symstyle == 2:
self._path = Path.unit_regular_asterisk(numsides)
self._filled = False
elif symstyle == 3:
self._path = Path.unit_circle()
self._transform = Affine2D().scale(0.5).rotate_deg(rotation)
else:
verts = np.asarray(marker[0])
path = Path(verts)
self._set_custom_marker(path)
def _set_mathtext_path(self):
"""
Draws mathtext markers '$...$' using TextPath object.
Submitted by tcb
"""
from matplotlib.patches import PathPatch
from matplotlib.text import TextPath
from matplotlib.font_manager import FontProperties
# again, the properties could be initialised just once outside
# this function
# Font size is irrelevant here, it will be rescaled based on
# the drawn size later
props = FontProperties(size=1.0)
text = TextPath(xy=(0,0), s=self.get_marker(), fontproperties=props,
usetex=rcParams['text.usetex'])
if len(text.vertices) == 0:
return
xmin, ymin = text.vertices.min(axis=0)
xmax, ymax = text.vertices.max(axis=0)
width = xmax - xmin
height = ymax - ymin
max_dim = max(width, height)
self._transform = Affine2D() \
.translate(-xmin + 0.5 * -width, -ymin + 0.5 * -height) \
.scale(1.0 / max_dim)
self._path = text
self._snap = False
def _set_circle(self, reduction = 1.0):
self._transform = Affine2D().scale(0.5 * reduction)
self._snap_threshold = 3.0
fs = self.get_fillstyle()
if fs=='full':
self._path = Path.unit_circle()
else:
# build a right-half circle
if fs=='bottom': rotate = 270.
elif fs=='top': rotate = 90.
elif fs=='left': rotate = 180.
else: rotate = 0.
self._path = self._alt_path = Path.unit_circle_righthalf()
self._transform.rotate_deg(rotate)
self._alt_transform = self._transform.frozen().rotate_deg(180.)
def _set_pixel(self):
self._path = Path.unit_rectangle()
self._transform = Affine2D().translate(-0.5, 0.5)
self._snap_threshold = False
def _set_point(self):
self._set_circle(reduction = self._point_size_reduction)
_triangle_path = Path(
[[0.0, 1.0], [-1.0, -1.0], [1.0, -1.0], [0.0, 1.0]],
[Path.MOVETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY])
# Going down halfway looks to small. Golden ratio is too far.
_triangle_path_u = Path(
[[0.0, 1.0], [-3/5., -1/5.], [3/5., -1/5.], [0.0, 1.0]],
[Path.MOVETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY])
_triangle_path_d = Path(
[[-3/5., -1/5.], [3/5., -1/5.], [1.0, -1.0], [-1.0, -1.0], [-3/5., -1/5.]],
[Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY])
_triangle_path_l = Path(
[[0.0, 1.0], [0.0, -1.0], [-1.0, -1.0], [0.0, 1.0]],
[Path.MOVETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY])
_triangle_path_r = Path(
[[0.0, 1.0], [0.0, -1.0], [1.0, -1.0], [0.0, 1.0]],
[Path.MOVETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY])
def _set_triangle(self, rot, skip):
self._transform = Affine2D().scale(0.5, 0.5).rotate_deg(rot)
self._snap_threshold = 5.0
fs = self.get_fillstyle()
if fs=='full':
self._path = self._triangle_path
else:
mpaths = [self._triangle_path_u,
self._triangle_path_l,
self._triangle_path_d,
self._triangle_path_r]
if fs=='top':
self._path = mpaths[(0+skip) % 4]
self._alt_path = mpaths[(2+skip) % 4]
elif fs=='bottom':
self._path = mpaths[(2+skip) % 4]
self._alt_path = mpaths[(0+skip) % 4]
elif fs=='left':
self._path = mpaths[(1+skip) % 4]
self._alt_path = mpaths[(3+skip) % 4]
else:
self._path = mpaths[(3+skip) % 4]
self._alt_path = mpaths[(1+skip) % 4]
self._alt_transform = self._transform
def _set_triangle_up(self):
return self._set_triangle(0.0, 0)
def _set_triangle_down(self):
return self._set_triangle(180.0, 2)
def _set_triangle_left(self):
return self._set_triangle(90.0, 3)
def _set_triangle_right(self):
return self._set_triangle(270.0, 1)
def _set_square(self):
self._transform = Affine2D().translate(-0.5, -0.5)
self._snap_threshold = 2.0
fs = self.get_fillstyle()
if fs=='full':
self._path = Path.unit_rectangle()
else:
# build a bottom filled square out of two rectangles, one
# filled. Use the rotation to support left, right, bottom
# or top
if fs=='bottom': rotate = 0.
elif fs=='top': rotate = 180.
elif fs=='left': rotate = 270.
else: rotate = 90.
self._path = Path([[0.0, 0.0], [1.0, 0.0], [1.0, 0.5], [0.0, 0.5], [0.0, 0.0]])
self._alt_path = Path([[0.0, 0.5], [1.0, 0.5], [1.0, 1.0], [0.0, 1.0], [0.0, 0.5]])
self._transform.rotate_deg(rotate)
self._alt_transform = self._transform
def _set_diamond(self):
self._transform = Affine2D().translate(-0.5, -0.5).rotate_deg(45)
self._snap_threshold = 5.0
fs = self.get_fillstyle()
if fs=='full':
self._path = Path.unit_rectangle()
else:
self._path = Path([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 0.0]])
self._alt_path = Path([[0.0, 0.0], [0.0, 1.0], [1.0, 1.0], [0.0, 0.0]])
if fs=='bottom': rotate = 270.
elif fs=='top': rotate = 90.
elif fs=='left': rotate = 180.
else: rotate = 0.
self._transform.rotate_deg(rotate)
self._alt_transform = self._transform
def _set_thin_diamond(self):
self._set_diamond()
self._transform.scale(0.6, 1.0)
def _set_pentagon(self):
self._transform = Affine2D().scale(0.5)
self._snap_threshold = 5.0
polypath = Path.unit_regular_polygon(5)
fs = self.get_fillstyle()
if fs == 'full':
self._path = polypath
else:
verts = polypath.vertices
y = (1+np.sqrt(5))/4.
top = Path([verts[0], verts[1], verts[4], verts[0]])
bottom = Path([verts[1], verts[2], verts[3], verts[4], verts[1]])
left = Path([verts[0], verts[1], verts[2], [0,-y], verts[0]])
right = Path([verts[0], verts[4], verts[3], [0,-y], verts[0]])
if fs == 'top':
mpath, mpath_alt = top, bottom
elif fs == 'bottom':
mpath, mpath_alt = bottom, top
elif fs == 'left':
mpath, mpath_alt = left, right
else:
mpath, mpath_alt = right, left
self._path = mpath
self._alt_path = mpath_alt
self._alt_transform = self._transform
def _set_star(self):
self._transform = Affine2D().scale(0.5)
self._snap_threshold = 5.0
fs = self.get_fillstyle()
polypath = Path.unit_regular_star(5, innerCircle=0.381966)
if fs == 'full':
self._path = polypath
else:
verts = polypath.vertices
top = Path(np.vstack((verts[0:4,:], verts[7:10,:], verts[0])))
bottom = Path(np.vstack((verts[3:8,:], verts[3])))
left = Path(np.vstack((verts[0:6,:], verts[0])))
right = Path(np.vstack((verts[0], verts[5:10,:], verts[0])))
if fs == 'top':
mpath, mpath_alt = top, bottom
elif fs == 'bottom':
mpath, mpath_alt = bottom, top
elif fs == 'left':
mpath, mpath_alt = left, right
else:
mpath, mpath_alt = right, left
self._path = mpath
self._alt_path = mpath_alt
self._alt_transform = self._transform
def _set_hexagon1(self):
self._transform = Affine2D().scale(0.5)
self._snap_threshold = 5.0
fs = self.get_fillstyle()
polypath = Path.unit_regular_polygon(6)
if fs == 'full':
self._path = polypath
else:
verts = polypath.vertices
# not drawing inside lines
x = np.abs(np.cos(5*np.pi/6.))
top = Path(np.vstack(([-x,0],verts[(1,0,5),:],[x,0])))
bottom = Path(np.vstack(([-x,0],verts[2:5,:],[x,0])))
left = Path(verts[(0,1,2,3),:])
right = Path(verts[(0,5,4,3),:])
if fs == 'top':
mpath, mpath_alt = top, bottom
elif fs == 'bottom':
mpath, mpath_alt = bottom, top
elif fs == 'left':
mpath, mpath_alt = left, right
else:
mpath, mpath_alt = right, left
self._path = mpath
self._alt_path = mpath_alt
self._alt_transform = self._transform
def _set_hexagon2(self):
self._transform = Affine2D().scale(0.5).rotate_deg(30)
self._snap_threshold = 5.0
fs = self.get_fillstyle()
polypath = Path.unit_regular_polygon(6)
if fs == 'full':
self._path = polypath
else:
verts = polypath.vertices
# not drawing inside lines
x, y = np.sqrt(3)/4, 3/4.
top = Path(verts[(1,0,5,4,1),:])
bottom = Path(verts[(1,2,3,4),:])
left = Path(np.vstack(([x,y],verts[(0,1,2),:],[-x,-y],[x,y])))
right = Path(np.vstack(([x,y],verts[(5,4,3),:],[-x,-y])))
if fs == 'top':
mpath, mpath_alt = top, bottom
elif fs == 'bottom':
mpath, mpath_alt = bottom, top
elif fs == 'left':
mpath, mpath_alt = left, right
else:
mpath, mpath_alt = right, left
self._path = mpath
self._alt_path = mpath_alt
self._alt_transform = self._transform
def _set_octagon(self):
self._transform = Affine2D().scale(0.5)
self._snap_threshold = 5.0
fs = self.get_fillstyle()
polypath = Path.unit_regular_polygon(8)
if fs == 'full':
self._transform.rotate_deg(22.5)
self._path = polypath
else:
x = np.sqrt(2.)/4.
half = Path([[0, -1], [0, 1], [-x, 1], [-1, x],
[-1, -x], [-x, -1], [0, -1]])
if fs=='bottom': rotate = 90.
elif fs=='top': rotate = 270.
elif fs=='right': rotate = 180.
else: rotate = 0.
self._transform.rotate_deg(rotate)
self._path = self._alt_path = half
self._alt_transform = self._transform.frozen().rotate_deg(180.0)
_line_marker_path = Path([[0.0, -1.0], [0.0, 1.0]])
def _set_vline(self):
self._transform = Affine2D().scale(0.5)
self._snap_threshold = 1.0
self._filled = False
self._path = self._line_marker_path
def _set_hline(self):
self._transform = Affine2D().scale(0.5).rotate_deg(90)
self._snap_threshold = 1.0
self._filled = False
self._path = self._line_marker_path
_tickhoriz_path = Path([[0.0, 0.0], [1.0, 0.0]])
def _set_tickleft(self):
self._transform = Affine2D().scale(-1.0, 1.0)
self._snap_threshold = 1.0
self._filled = False
self._path = self._tickhoriz_path
def _set_tickright(self):
self._transform = Affine2D().scale(1.0, 1.0)
self._snap_threshold = 1.0
self._filled = False
self._path = self._tickhoriz_path
_tickvert_path = Path([[-0.0, 0.0], [-0.0, 1.0]])
def _set_tickup(self):
self._transform = Affine2D().scale(1.0, 1.0)
self._snap_threshold = 1.0
self._filled = False
self._path = self._tickvert_path
def _set_tickdown(self):
self._transform = Affine2D().scale(1.0, -1.0)
self._snap_threshold = 1.0
self._filled = False
self._path = self._tickvert_path
_plus_path = Path([[-1.0, 0.0], [1.0, 0.0],
[0.0, -1.0], [0.0, 1.0]],
[Path.MOVETO, Path.LINETO,
Path.MOVETO, Path.LINETO])
def _set_plus(self):
self._transform = Affine2D().scale(0.5)
self._snap_threshold = 1.0
self._filled = False
self._path = self._plus_path
_tri_path = Path([[0.0, 0.0], [0.0, -1.0],
[0.0, 0.0], [0.8, 0.5],
[0.0, 0.0], [-0.8, 0.5]],
[Path.MOVETO, Path.LINETO,
Path.MOVETO, Path.LINETO,
Path.MOVETO, Path.LINETO])
def _set_tri_down(self):
self._transform = Affine2D().scale(0.5)
self._snap_threshold = 5.0
self._filled = False
self._path = self._tri_path
def _set_tri_up(self):
self._transform = Affine2D().scale(0.5).rotate_deg(90)
self._snap_threshold = 5.0
self._filled = False
self._path = self._tri_path
def _set_tri_left(self):
self._transform = Affine2D().scale(0.5).rotate_deg(270)
self._snap_threshold = 5.0
self._filled = False
self._path = self._tri_path
def _set_tri_right(self):
self._transform = Affine2D().scale(0.5).rotate_deg(180)
self._snap_threshold = 5.0
self._filled = False
self._path = self._tri_path
_caret_path = Path([[-1.0, 1.5], [0.0, 0.0], [1.0, 1.5]])
def _set_caretdown(self):
self._transform = Affine2D().scale(0.5)
self._snap_threshold = 3.0
self._filled = False
self._path = self._caret_path
def _set_caretup(self):
self._transform = Affine2D().scale(0.5).rotate_deg(180)
self._snap_threshold = 3.0
self._filled = False
self._path = self._caret_path
def _set_caretleft(self):
self._transform = Affine2D().scale(0.5).rotate_deg(270)
self._snap_threshold = 3.0
self._filled = False
self._path = self._caret_path
def _set_caretright(self):
self._transform = Affine2D().scale(0.5).rotate_deg(90)
self._snap_threshold = 3.0
self._filled = False
self._path = self._caret_path
_x_path = Path([[-1.0, -1.0], [1.0, 1.0],
[-1.0, 1.0], [1.0, -1.0]],
[Path.MOVETO, Path.LINETO,
Path.MOVETO, Path.LINETO])
def _set_x(self):
self._transform = Affine2D().scale(0.5)
self._snap_threshold = 3.0
self._filled = False
self._path = self._x_path
_styles = [(repr(x), y) for x, y in MarkerStyle.markers.items()]
_styles.sort(lambda x, y: cmp(x[1], y[1]))
MarkerStyle.style_table = (
MarkerStyle.style_table %
'\n'.join(['%-30s %-33s' % ('``%s``' % x, y) for (x, y) in _styles]))
MarkerStyle.accepts = textwrap.fill(
MarkerStyle.accepts %
' | '.join(['``%s``' % x for (x, y) in _styles]))
docstring.interpd.update(MarkerTable=MarkerStyle.style_table)
docstring.interpd.update(MarkerAccepts=MarkerStyle.accepts)