# -*- coding: utf-8 -*- from __future__ import division import math import matplotlib as mpl import numpy as np import matplotlib.cbook as cbook import matplotlib.artist as artist import matplotlib.colors as colors import matplotlib.transforms as transforms import matplotlib.artist as artist from matplotlib.path import Path # these are not available for the object inspector until after the # class is built so we define an initial set here for the init # function and they will be overridden after object definition artist.kwdocd['Patch'] = """ ================= ============================================== Property Description ================= ============================================== alpha float animated [True | False] antialiased or aa [True | False] clip_box a matplotlib.transform.Bbox instance clip_on [True | False] edgecolor or ec any matplotlib color facecolor or fc any matplotlib color figure a matplotlib.figure.Figure instance fill [True | False] hatch unknown label any string linewidth or lw float lod [True | False] transform a matplotlib.transform transformation instance visible [True | False] zorder any number ================= ============================================== """ 00043 class Patch(artist.Artist): """ A patch is a 2D thingy with a face color and an edge color. If any of *edgecolor*, *facecolor*, *linewidth*, or *antialiased* are *None*, they default to their rc params setting. """ zorder = 1 def __str__(self): return str(self.__class__).split('.')[-1] 00054 def __init__(self, edgecolor=None, facecolor=None, linewidth=None, linestyle=None, antialiased = None, hatch = None, fill=True, **kwargs ): """ The following kwarg properties are supported %(Patch)s """ artist.Artist.__init__(self) if linewidth is None: linewidth = mpl.rcParams['patch.linewidth'] if linestyle is None: linestyle = "solid" if antialiased is None: antialiased = mpl.rcParams['patch.antialiased'] self.set_edgecolor(edgecolor) self.set_facecolor(facecolor) self.set_linewidth(linewidth) self.set_linestyle(linestyle) self.set_antialiased(antialiased) self.set_hatch(hatch) self.fill = fill self._combined_transform = transforms.IdentityTransform() if len(kwargs): artist.setp(self, **kwargs) __init__.__doc__ = cbook.dedent(__init__.__doc__) % artist.kwdocd 00087 def get_verts(self): """ Return a copy of the vertices used in this patch If the patch contains Bézier curves, the curves will be interpolated by line segments. To access the curves as curves, use :meth:`get_path`. """ trans = self.get_transform() path = self.get_path() polygons = path.to_polygons(trans) if len(polygons): return polygons[0] return [] 00102 def contains(self, mouseevent): """Test whether the mouse event occurred in the patch. Returns T/F, {} """ # This is a general version of contains that should work on any # patch with a path. However, patches that have a faster # algebraic solution to hit-testing should override this # method. if callable(self._contains): return self._contains(self,mouseevent) inside = self.get_path().contains_point( (mouseevent.x, mouseevent.y), self.get_transform()) return inside, {} def update_from(self, other): artist.Artist.update_from(self, other) self.set_edgecolor(other.get_edgecolor()) self.set_facecolor(other.get_facecolor()) self.set_fill(other.get_fill()) self.set_hatch(other.get_hatch()) self.set_linewidth(other.get_linewidth()) self.set_linestyle(other.get_linestyle()) self.set_transform(other.get_data_transform()) self.set_figure(other.get_figure()) self.set_alpha(other.get_alpha()) def get_extents(self): return self.get_path().get_extents(self.get_transform()) 00132 def get_transform(self): return self.get_patch_transform() + artist.Artist.get_transform(self) def get_data_transform(self): return artist.Artist.get_transform(self) def get_patch_transform(self): return transforms.IdentityTransform() def get_antialiased(self): return self._antialiased get_aa = get_antialiased def get_edgecolor(self): return self._edgecolor get_ec = get_edgecolor def get_facecolor(self): return self._facecolor get_fc = get_facecolor def get_linewidth(self): return self._linewidth get_lw = get_linewidth def get_linestyle(self): return self._linestyle get_ls = get_linestyle 00161 def set_antialiased(self, aa): """ Set whether to use antialiased rendering ACCEPTS: [True | False] or None for default """ if aa is None: aa = mpl.rcParams['patch.antialiased'] self._antialiased = aa set_aa = set_antialiased 00171 def set_edgecolor(self, color): """ Set the patch edge color ACCEPTS: mpl color spec, or None for default, or 'none' for no color """ if color is None: color = mpl.rcParams['patch.edgecolor'] self._edgecolor = color set_ec = set_edgecolor 00181 def set_facecolor(self, color): """ Set the patch face color ACCEPTS: mpl color spec, or None for default, or 'none' for no color """ if color is None: color = mpl.rcParams['patch.facecolor'] self._facecolor = color set_fc = set_facecolor 00191 def set_linewidth(self, w): """ Set the patch linewidth in points ACCEPTS: float or None for default """ if w is None: w = mpl.rcParams['patch.linewidth'] self._linewidth = w set_lw = set_linewidth 00201 def set_linestyle(self, ls): """ Set the patch linestyle ACCEPTS: ['solid' | 'dashed' | 'dashdot' | 'dotted'] """ if ls is None: ls = "solid" self._linestyle = ls set_ls = set_linestyle 00211 def set_fill(self, b): """ Set whether to fill the patch ACCEPTS: [True | False] """ self.fill = b def get_fill(self): 'return whether fill is set' return self.fill 00223 def set_hatch(self, h): """ Set the hatching pattern hatch can be one of:: / - diagonal hatching \ - back diagonal | - vertical - - horizontal # - crossed x - crossed diagonal Letters can be combined, in which case all the specified hatchings are done. If same letter repeats, it increases the density of hatching in that direction. CURRENT LIMITATIONS: 1. Hatching is supported in the PostScript backend only. 2. Hatching is done with solid black lines of width 0. """ self._hatch = h def get_hatch(self): 'return the current hatching pattern' return self._hatch def draw(self, renderer): if not self.get_visible(): return #renderer.open_group('patch') gc = renderer.new_gc() if cbook.is_string_like(self._edgecolor) and self._edgecolor.lower()=='none': gc.set_linewidth(0) else: gc.set_foreground(self._edgecolor) gc.set_linewidth(self._linewidth) gc.set_linestyle(self._linestyle) gc.set_alpha(self._alpha) gc.set_antialiased(self._antialiased) self._set_gc_clip(gc) gc.set_capstyle('projecting') if (not self.fill or self._facecolor is None or (cbook.is_string_like(self._facecolor) and self._facecolor.lower()=='none')): rgbFace = None else: rgbFace = colors.colorConverter.to_rgb(self._facecolor) if self._hatch: gc.set_hatch(self._hatch ) path = self.get_path() transform = self.get_transform() tpath = transform.transform_path_non_affine(path) affine = transform.get_affine() renderer.draw_path(gc, tpath, affine, rgbFace) #renderer.close_group('patch') 00289 def get_path(self): """ Return the path of this patch """ raise NotImplementedError('Derived must override') def get_window_extent(self, renderer=None): return self.get_path().get_extents(self.get_transform()) class Shadow(Patch): def __str__(self): return "Shadow(%s)"%(str(self.patch)) def __init__(self, patch, ox, oy, props=None, **kwargs): """ Create a shadow of the given *patch* offset by *ox*, *oy*. *props*, if not *None*, is a patch property update dictionary. If *None*, the shadow will have have the same color as the face, but darkened. kwargs are %(Patch)s """ Patch.__init__(self) self.patch = patch self.props = props self._ox, self._oy = ox, oy self._update_transform() self._update() __init__.__doc__ = cbook.dedent(__init__.__doc__) % artist.kwdocd def _update(self): self.update_from(self.patch) if self.props is not None: self.update(self.props) else: r,g,b,a = colors.colorConverter.to_rgba(self.patch.get_facecolor()) rho = 0.3 r = rho*r g = rho*g b = rho*b self.set_facecolor((r,g,b,0.5)) self.set_edgecolor((r,g,b,0.5)) def _update_transform(self): self._shadow_transform = transforms.Affine2D().translate(self._ox, self._oy) def _get_ox(self): return self._ox def _set_ox(self, ox): self._ox = ox self._update_transform() def _get_oy(self): return self._oy def _set_oy(self, oy): self._oy = oy self._update_transform() def get_path(self): return self.patch.get_path() def get_patch_transform(self): return self.patch.get_patch_transform() + self._shadow_transform 00356 class Rectangle(Patch): """ Draw a rectangle with lower left at *xy*=(*x*, *y*) with specified width and height """ def __str__(self): return self.__class__.__name__ \ + "(%g,%g;%gx%g)" % (self._x, self._y, self._width, self._height) 00366 def __init__(self, xy, width, height, **kwargs): """ *fill* is a boolean indicating whether to fill the rectangle Valid kwargs are: %(Patch)s """ Patch.__init__(self, **kwargs) self._x = xy[0] self._y = xy[1] self._width = width self._height = height self._rect_transform = transforms.IdentityTransform() self._update_patch_transform() __init__.__doc__ = cbook.dedent(__init__.__doc__) % artist.kwdocd 00385 def get_path(self): """ Return the vertices of the rectangle """ return Path.unit_rectangle() def _update_patch_transform(self): x = self.convert_xunits(self._x) y = self.convert_yunits(self._y) width = self.convert_xunits(self._width) height = self.convert_yunits(self._height) bbox = transforms.Bbox.from_bounds(x, y, width, height) self._rect_transform = transforms.BboxTransformTo(bbox) def get_patch_transform(self): self._update_patch_transform() return self._rect_transform 00403 def contains(self, mouseevent): # special case the degernate rectangle if self._width==0 or self._height==0: return False, {} x, y = self.get_transform().inverted().transform_point( (mouseevent.x, mouseevent.y)) return (x >= 0.0 and x <= 1.0 and y >= 0.0 and y <= 1.0), {} def get_x(self): "Return the left coord of the rectangle" return self._x def get_y(self): "Return the bottom coord of the rectangle" return self._y def get_width(self): "Return the width of the rectangle" return self._width def get_height(self): "Return the height of the rectangle" return self._height 00428 def set_x(self, x): """ Set the left coord of the rectangle ACCEPTS: float """ self._x = x 00436 def set_y(self, y): """ Set the bottom coord of the rectangle ACCEPTS: float """ self._y = y 00444 def set_width(self, w): """ Set the width rectangle ACCEPTS: float """ self._width = w 00452 def set_height(self, h): """ Set the width rectangle ACCEPTS: float """ self._height = h 00460 def set_bounds(self, *args): """ Set the bounds of the rectangle: l,b,w,h ACCEPTS: (left, bottom, width, height) """ if len(args)==0: l,b,w,h = args[0] else: l,b,w,h = args self._x = l self._y = b self._width = w self._height = h def get_bbox(self): return transforms.Bbox.from_bounds(self._x, self._y, self._width, self._height) 00478 class RegularPolygon(Patch): """ A regular polygon patch. """ def __str__(self): return "Poly%d(%g,%g)"%(self._numVertices,self._xy[0],self._xy[1]) 00485 def __init__(self, xy, numVertices, radius=5, orientation=0, **kwargs): """ Constructor arguments: *xy* A length 2 tuple (*x*, *y*) of the center. *numVertices* the number of vertices. *radius* The distance from the center to each of the vertices. *orientation* rotates the polygon (in radians). Valid kwargs are: %(Patch)s """ self._xy = xy self._numVertices = numVertices self._orientation = orientation self._radius = radius self._path = Path.unit_regular_polygon(numVertices) self._poly_transform = transforms.Affine2D() self._update_transform() Patch.__init__(self, **kwargs) __init__.__doc__ = cbook.dedent(__init__.__doc__) % artist.kwdocd def _update_transform(self): self._poly_transform.clear() \ .scale(self.radius) \ .rotate(self.orientation) \ .translate(*self.xy) def _get_xy(self): return self._xy def _set_xy(self, xy): self._update_transform() xy = property(_get_xy, _set_xy) def _get_orientation(self): return self._orientation def _set_orientation(self, xy): self._orientation = xy orientation = property(_get_orientation, _set_orientation) def _get_radius(self): return self._radius def _set_radius(self, xy): self._radius = xy radius = property(_get_radius, _set_radius) def _get_numvertices(self): return self._numVertices def _set_numvertices(self, numVertices): self._numVertices = numVertices numvertices = property(_get_numvertices, _set_numvertices) 00547 def get_path(self): return self._path def get_patch_transform(self): self._update_transform() return self._poly_transform 00554 class PathPatch(Patch): """ A general polycurve path patch. """ def __str__(self): return "Poly((%g, %g) ...)" % tuple(self._path.vertices[0]) 00561 def __init__(self, path, **kwargs): """ *path* is a :class:`matplotlib.path.Path` object. Valid kwargs are: %(Patch)s See Patch documentation for additional kwargs """ Patch.__init__(self, **kwargs) self._path = path __init__.__doc__ = cbook.dedent(__init__.__doc__) % artist.kwdocd 00573 def get_path(self): return self._path 00576 class Polygon(Patch): """ A general polygon patch. """ def __str__(self): return "Poly((%g, %g) ...)" % tuple(self._path.vertices[0]) 00583 def __init__(self, xy, closed=True, **kwargs): """ *xy* is a numpy array with shape Nx2. If *closed* is *True*, the polygon will be closed so the starting and ending points are the same. Valid kwargs are: %(Patch)s See Patch documentation for additional kwargs """ Patch.__init__(self, **kwargs) xy = np.asarray(xy, np.float_) self._path = Path(xy) self.set_closed(closed) __init__.__doc__ = cbook.dedent(__init__.__doc__) % artist.kwdocd 00601 def get_path(self): return self._path def get_closed(self): return self._closed def set_closed(self, closed): self._closed = closed xy = self._get_xy() if closed: if len(xy) and (xy[0] != xy[-1]).any(): xy = np.concatenate([xy, [xy[0]]]) else: if len(xy)>2 and (xy[0]==xy[-1]).all(): xy = xy[0:-1] self._set_xy(xy) def get_xy(self): return self._path.vertices def set_xy(self, vertices): self._path = Path(vertices) _get_xy = get_xy _set_xy = set_xy xy = property( get_xy, set_xy, None, """Set/get the vertices of the polygon. This property is provided for backward compatibility with matplotlib 0.91.x only. New code should use :meth:`~matplotlib.patches.Polygon.get_xy` and :meth:`~matplotlib.patches.Polygon.set_xy` instead.""") class Wedge(Patch): def __str__(self): return "Wedge(%g,%g)"%(self.theta1,self.theta2) def __init__(self, center, r, theta1, theta2, **kwargs): """ Draw a wedge centered at *x*, *y* center with radius *r* that sweeps *theta1* to *theta2* (in degrees). Valid kwargs are: %(Patch)s """ Patch.__init__(self, **kwargs) self.center = center self.r = r self.theta1 = theta1 self.theta2 = theta2 self._patch_transform = transforms.IdentityTransform() self._path = Path.wedge(self.theta1, self.theta2) __init__.__doc__ = cbook.dedent(__init__.__doc__) % artist.kwdocd def get_path(self): return self._path def get_patch_transform(self): x = self.convert_xunits(self.center[0]) y = self.convert_yunits(self.center[1]) rx = self.convert_xunits(self.r) ry = self.convert_yunits(self.r) self._patch_transform = transforms.Affine2D() \ .scale(rx, ry).translate(x, y) return self._patch_transform # COVERAGE NOTE: Not used internally or from examples 00667 class Arrow(Patch): """ An arrow patch. """ def __str__(self): return "Arrow()" _path = Path( [ [ 0.0, 0.1 ], [ 0.0, -0.1], [ 0.8, -0.1 ], [ 0.8, -0.3], [ 1.0, 0.0 ], [ 0.8, 0.3], [ 0.8, 0.1 ], [ 0.0, 0.1] ] ) 00680 def __init__( self, x, y, dx, dy, width=1.0, **kwargs ): """ Draws an arrow, starting at (*x*, *y*), direction and length given by (*dx*, *dy*) the width of the arrow is scaled by *width*. Valid kwargs are: %(Patch)s """ Patch.__init__(self, **kwargs) L = np.sqrt(dx**2+dy**2) or 1 # account for div by zero cx = float(dx)/L sx = float(dy)/L trans1 = transforms.Affine2D().scale(L, width) trans2 = transforms.Affine2D.from_values(cx, sx, -sx, cx, 0.0, 0.0) trans3 = transforms.Affine2D().translate(x, y) trans = trans1 + trans2 + trans3 self._patch_transform = trans.frozen() __init__.__doc__ = cbook.dedent(__init__.__doc__) % artist.kwdocd 00700 def get_path(self): return self._path def get_patch_transform(self): return self._patch_transform 00706 class FancyArrow(Polygon): """ Like Arrow, but lets you set head width and head height independently. """ def __str__(self): return "FancyArrow()" 00714 def __init__(self, x, y, dx, dy, width=0.001, length_includes_head=False, \ head_width=None, head_length=None, shape='full', overhang=0, \ head_starts_at_zero=False,**kwargs): """ Constructor arguments *length_includes_head*: *True* if head is counted in calculating the length. *shape*: ['full', 'left', 'right'] *overhang*: distance that the arrow is swept back (0 overhang means triangular shape). *head_starts_at_zero*: If *True*, the head starts being drawn at coordinate 0 instead of ending at coordinate 0. Valid kwargs are: %(Patch)s """ if head_width is None: head_width = 3 * width if head_length is None: head_length = 1.5 * head_width distance = np.sqrt(dx**2 + dy**2) if length_includes_head: length=distance else: length=distance+head_length if not length: verts = [] #display nothing if empty else: #start by drawing horizontal arrow, point at (0,0) hw, hl, hs, lw = head_width, head_length, overhang, width left_half_arrow = np.array([ [0.0,0.0], #tip [-hl, -hw/2.0], #leftmost [-hl*(1-hs), -lw/2.0], #meets stem [-length, -lw/2.0], #bottom left [-length, 0], ]) #if we're not including the head, shift up by head length if not length_includes_head: left_half_arrow += [head_length, 0] #if the head starts at 0, shift up by another head length if head_starts_at_zero: left_half_arrow += [head_length/2.0, 0] #figure out the shape, and complete accordingly if shape == 'left': coords = left_half_arrow else: right_half_arrow = left_half_arrow*[1,-1] if shape == 'right': coords = right_half_arrow elif shape == 'full': coords=np.concatenate([left_half_arrow,right_half_arrow[::-1]]) else: raise ValueError, "Got unknown shape: %s" % shape cx = float(dx)/distance sx = float(dy)/distance M = np.array([[cx, sx],[-sx,cx]]) verts = np.dot(coords, M) + (x+dx, y+dy) Polygon.__init__(self, map(tuple, verts), **kwargs) __init__.__doc__ = cbook.dedent(__init__.__doc__) % artist.kwdocd 00784 class YAArrow(Patch): """ Yet another arrow class. This is an arrow that is defined in display space and has a tip at *x1*, *y1* and a base at *x2*, *y2*. """ def __str__(self): return "YAArrow()" 00794 def __init__(self, figure, xytip, xybase, width=4, frac=0.1, headwidth=12, **kwargs): """ Constructor arguments: *xytip* (*x*, *y*) location of arrow tip *xybase* (*x*, *y*) location the arrow base mid point *figure* The :class:`~matplotlib.figure.Figure` instance (fig.dpi) *width* The width of the arrow in points *frac* The fraction of the arrow length occupied by the head *headwidth* The width of the base of the arrow head in points Valid kwargs are: %(Patch)s """ self.figure = figure self.xytip = xytip self.xybase = xybase self.width = width self.frac = frac self.headwidth = headwidth Patch.__init__(self, **kwargs) __init__.__doc__ = cbook.dedent(__init__.__doc__) % artist.kwdocd 00830 def get_path(self): # Since this is dpi dependent, we need to recompute the path # every time. # the base vertices x1, y1 = self.xytip x2, y2 = self.xybase k1 = self.width*self.figure.dpi/72./2. k2 = self.headwidth*self.figure.dpi/72./2. xb1, yb1, xb2, yb2 = self.getpoints(x1, y1, x2, y2, k1) # a point on the segment 20% of the distance from the tip to the base theta = math.atan2(y2-y1, x2-x1) r = math.sqrt((y2-y1)**2. + (x2-x1)**2.) xm = x1 + self.frac * r * math.cos(theta) ym = y1 + self.frac * r * math.sin(theta) xc1, yc1, xc2, yc2 = self.getpoints(x1, y1, xm, ym, k1) xd1, yd1, xd2, yd2 = self.getpoints(x1, y1, xm, ym, k2) xs = self.convert_xunits([xb1, xb2, xc2, xd2, x1, xd1, xc1, xb1]) ys = self.convert_yunits([yb1, yb2, yc2, yd2, y1, yd1, yc1, yb1]) return Path(zip(xs, ys)) def get_patch_transform(self): return transforms.IdentityTransform() 00857 def getpoints(self, x1,y1,x2,y2, k): """ For line segment defined by (*x1*, *y1*) and (*x2*, *y2*) return the points on the line that is perpendicular to the line and intersects (*x2*, *y2*) and the distance from (*x2*, *y2*) of the returned points is *k*. """ x1,y1,x2,y2,k = map(float, (x1,y1,x2,y2,k)) if y2-y1 == 0: return x2, y2+k, x2, y2-k elif x2-x1 == 0: return x2+k, y2, x2-k, y2 m = (y2-y1)/(x2-x1) pm = -1./m a = 1 b = -2*y2 c = y2**2. - k**2.*pm**2./(1. + pm**2.) y3a = (-b + math.sqrt(b**2.-4*a*c))/(2.*a) x3a = (y3a - y2)/pm + x2 y3b = (-b - math.sqrt(b**2.-4*a*c))/(2.*a) x3b = (y3b - y2)/pm + x2 return x3a, y3a, x3b, y3b 00885 class CirclePolygon(RegularPolygon): """ A polygon-approximation of a circle patch. """ def __str__(self): return "CirclePolygon(%d,%d)"%self.center 00892 def __init__(self, xy, radius=5, resolution=20, # the number of vertices **kwargs): """ Create a circle at *xy* = (*x*, *y*) with given *radius*. This circle is approximated by a regular polygon with *resolution* sides. For a smoother circle drawn with splines, see :class:`~matplotlib.patches.Circle`. Valid kwargs are: %(Patch)s """ RegularPolygon.__init__(self, xy, resolution, radius, orientation=0, **kwargs) __init__.__doc__ = cbook.dedent(__init__.__doc__) % artist.kwdocd 00913 class Ellipse(Patch): """ A scale-free ellipse. """ def __str__(self): return "Ellipse(%s,%s;%sx%s)"%(self.center[0],self.center[1],self.width,self.height) 00920 def __init__(self, xy, width, height, angle=0.0, **kwargs): """ *xy* center of ellipse *width* length of horizontal axis *height* length of vertical axis *angle* rotation in degrees (anti-clockwise) Valid kwargs are: %(Patch)s """ Patch.__init__(self, **kwargs) self.center = xy self.width, self.height = width, height self.angle = angle self._path = Path.unit_circle() self._patch_transform = transforms.IdentityTransform() self._recompute_transform() __init__.__doc__ = cbook.dedent(__init__.__doc__) % artist.kwdocd def _recompute_transform(self): center = (self.convert_xunits(self.center[0]), self.convert_yunits(self.center[1])) width = self.convert_xunits(self.width) height = self.convert_yunits(self.height) self._patch_transform = transforms.Affine2D() \ .scale(width * 0.5, height * 0.5) \ .rotate_deg(self.angle) \ .translate(*center) 00957 def get_path(self): """ Return the vertices of the rectangle """ return self._path def get_patch_transform(self): self._recompute_transform() return self._patch_transform 00967 def contains(self,ev): if ev.x is None or ev.y is None: return False,{} x, y = self.get_transform().inverted().transform_point((ev.x, ev.y)) return (x*x + y*y) <= 1.0, {} 00973 class Circle(Ellipse): """ A circle patch. """ def __str__(self): return "Circle((%g,%g),r=%g)"%(self.center[0],self.center[1],self.radius) 00980 def __init__(self, xy, radius=5, **kwargs): """ Create true circle at center *xy* = (*x*, *y*) with given *radius*. Unlike :class:`~matplotlib.patches.CirclePolygon` which is a polygonal approximation, this uses Bézier splines and is much closer to a scale-free circle. Valid kwargs are: %(Patch)s """ if kwargs.has_key('resolution'): import warnings warnings.warn('Circle is now scale free. Use CirclePolygon instead!', DeprecationWarning) kwargs.pop('resolution') self.radius = radius Ellipse.__init__(self, xy, radius*2, radius*2, **kwargs) __init__.__doc__ = cbook.dedent(__init__.__doc__) % artist.kwdocd 01000 class Arc(Ellipse): """ An elliptical arc. Because it performs various optimizations, it can not be filled. The arc must be used in an :class:`~matplotlib.axes.Axes` instance---it cannot be added directly to a :class:`~matplotlib.figure.Figure`---because it is optimized to only render the segments that are inside the axes bounding box with high resolution. """ def __str__(self): return "Arc(%s,%s;%sx%s)"%(self.center[0],self.center[1],self.width,self.height) 01014 def __init__(self, xy, width, height, angle=0.0, theta1=0.0, theta2=360.0, **kwargs): """ The following args are supported: *xy* center of ellipse *width* length of horizontal axis *height* length of vertical axis *angle* rotation in degrees (anti-clockwise) *theta1* starting angle of the arc in degrees *theta2* ending angle of the arc in degrees If *theta1* and *theta2* are not provided, the arc will form a complete ellipse. Valid kwargs are: %(Patch)s """ fill = kwargs.pop('fill') if fill: raise ValueError("Arc objects can not be filled") kwargs['fill'] = False Ellipse.__init__(self, xy, width, height, angle, **kwargs) self.theta1 = theta1 self.theta2 = theta2 __init__.__doc__ = cbook.dedent(__init__.__doc__) % artist.kwdocd 01054 def draw(self, renderer): """ Ellipses are normally drawn using an approximation that uses eight cubic bezier splines. The error of this approximation is 1.89818e-6, according to this unverified source: Lancaster, Don. Approximating a Circle or an Ellipse Using Four Bezier Cubic Splines. http://www.tinaja.com/glib/ellipse4.pdf There is a use case where very large ellipses must be drawn with very high accuracy, and it is too expensive to render the entire ellipse with enough segments (either splines or line segments). Therefore, in the case where either radius of the ellipse is large enough that the error of the spline approximation will be visible (greater than one pixel offset from the ideal), a different technique is used. In that case, only the visible parts of the ellipse are drawn, with each visible arc using a fixed number of spline segments (8). The algorithm proceeds as follows: 1. The points where the ellipse intersects the axes bounding box are located. (This is done be performing an inverse transformation on the axes bbox such that it is relative to the unit circle -- this makes the intersection calculation much easier than doing rotated ellipse intersection directly). This uses the "line intersecting a circle" algorithm from: Vince, John. Geometry for Computer Graphics: Formulae, Examples & Proofs. London: Springer-Verlag, 2005. 2. The angles of each of the intersection points are calculated. 3. Proceeding counterclockwise starting in the positive x-direction, each of the visible arc-segments between the pairs of vertices are drawn using the bezier arc approximation technique implemented in :meth:`matplotlib.path.Path.arc`. """ if not hasattr(self, 'axes'): raise RuntimeError('Arcs can only be used in Axes instances') self._recompute_transform() # Get the width and height in pixels width = self.convert_xunits(self.width) height = self.convert_yunits(self.height) width, height = self.get_transform().transform_point( (width, height)) inv_error = (1.0 / 1.89818e-6) * 0.5 if width < inv_error and height < inv_error: self._path = Path.arc(self.theta1, self.theta2) return Patch.draw(self, renderer) def iter_circle_intersect_on_line(x0, y0, x1, y1): dx = x1 - x0 dy = y1 - y0 dr2 = dx*dx + dy*dy D = x0*y1 - x1*y0 D2 = D*D discrim = dr2 - D2 # Single (tangential) intersection if discrim == 0.0: x = (D*dy) / dr2 y = (-D*dx) / dr2 yield x, y elif discrim > 0.0: # The definition of "sign" here is different from # np.sign: we never want to get 0.0 if dy < 0.0: sign_dy = -1.0 else: sign_dy = 1.0 sqrt_discrim = np.sqrt(discrim) for sign in (1., -1.): x = (D*dy + sign * sign_dy * dx * sqrt_discrim) / dr2 y = (-D*dx + sign * np.abs(dy) * sqrt_discrim) / dr2 yield x, y def iter_circle_intersect_on_line_seg(x0, y0, x1, y1): epsilon = 1e-9 if x1 < x0: x0e, x1e = x1, x0 else: x0e, x1e = x0, x1 if y1 < y0: y0e, y1e = y1, y0 else: y0e, y1e = y0, y1 x0e -= epsilon y0e -= epsilon x1e += epsilon y1e += epsilon for x, y in iter_circle_intersect_on_line(x0, y0, x1, y1): if x >= x0e and x <= x1e and y >= y0e and y <= y1e: yield x, y # Transforms the axes box_path so that it is relative to the unit # circle in the same way that it is relative to the desired # ellipse. box_path = Path.unit_rectangle() box_path_transform = transforms.BboxTransformTo(self.axes.bbox) + \ self.get_transform().inverted() box_path = box_path.transformed(box_path_transform) PI = np.pi TWOPI = PI * 2.0 RAD2DEG = 180.0 / PI DEG2RAD = PI / 180.0 theta1 = self.theta1 theta2 = self.theta2 thetas = {} # For each of the point pairs, there is a line segment for p0, p1 in zip(box_path.vertices[:-1], box_path.vertices[1:]): x0, y0 = p0 x1, y1 = p1 for x, y in iter_circle_intersect_on_line_seg(x0, y0, x1, y1): theta = np.arccos(x) if y < 0: theta = TWOPI - theta # Convert radians to angles theta *= RAD2DEG if theta > theta1 and theta < theta2: thetas[theta] = None thetas = thetas.keys() thetas.sort() thetas.append(theta2) last_theta = theta1 theta1_rad = theta1 * DEG2RAD inside = box_path.contains_point((np.cos(theta1_rad), np.sin(theta1_rad))) for theta in thetas: if inside: self._path = Path.arc(last_theta, theta, 8) Patch.draw(self, renderer) inside = False else: inside = True last_theta = theta def bbox_artist(artist, renderer, props=None, fill=True): """ This is a debug function to draw a rectangle around the bounding box returned by :meth:`~matplotlib.artist.Artist.get_window_extent` of an artist, to test whether the artist is returning the correct bbox. *props* is a dict of rectangle props with the additional property 'pad' that sets the padding around the bbox in points. """ if props is None: props = {} props = props.copy() # don't want to alter the pad externally pad = props.pop('pad', 4) pad = renderer.points_to_pixels(pad) bbox = artist.get_window_extent(renderer) l,b,w,h = bbox.bounds l-=pad/2. b-=pad/2. w+=pad h+=pad r = Rectangle(xy=(l,b), width=w, height=h, fill=fill, ) r.set_transform(transforms.IdentityTransform()) r.set_clip_on( False ) r.update(props) r.draw(renderer) def draw_bbox(bbox, renderer, color='k', trans=None): """ This is a debug function to draw a rectangle around the bounding box returned by :meth:`~matplotlib.artist.Artist.get_window_extent` of an artist, to test whether the artist is returning the correct bbox. """ l,b,w,h = bbox.get_bounds() r = Rectangle(xy=(l,b), width=w, height=h, edgecolor=color, fill=False, ) if trans is not None: r.set_transform(trans) r.set_clip_on( False ) r.draw(renderer) artist.kwdocd['Patch'] = patchdoc = artist.kwdoc(Patch) for k in ('Rectangle', 'Circle', 'RegularPolygon', 'Polygon', 'Wedge', 'Arrow', 'FancyArrow', 'YAArrow', 'CirclePolygon', 'Ellipse', 'Arc'): artist.kwdocd[k] = patchdoc