|
From: <jd...@us...> - 2008-10-30 13:18:15
|
Revision: 6353
http://matplotlib.svn.sourceforge.net/matplotlib/?rev=6353&view=rev
Author: jdh2358
Date: 2008-10-30 13:18:07 +0000 (Thu, 30 Oct 2008)
Log Message:
-----------
added jae-joons fancy arrow and box patch for annotations
Modified Paths:
--------------
trunk/matplotlib/CHANGELOG
trunk/matplotlib/lib/matplotlib/patches.py
trunk/matplotlib/lib/matplotlib/text.py
Added Paths:
-----------
trunk/matplotlib/examples/pylab_examples/annotation_demo2.py
trunk/matplotlib/lib/matplotlib/bezier.py
Modified: trunk/matplotlib/CHANGELOG
===================================================================
--- trunk/matplotlib/CHANGELOG 2008-10-29 20:28:57 UTC (rev 6352)
+++ trunk/matplotlib/CHANGELOG 2008-10-30 13:18:07 UTC (rev 6353)
@@ -1,3 +1,7 @@
+2008-10-24 Added Jae Joon's fancy arrow, box and annotation
+ enhancements -- see
+ examples/pylab_examples/annotation_demo2.py
+
2008-10-23 Autoscaling is now supported with shared axes - EF
2008-10-23 Fixed exception in dviread that happened with Minion - JKS
Added: trunk/matplotlib/examples/pylab_examples/annotation_demo2.py
===================================================================
--- trunk/matplotlib/examples/pylab_examples/annotation_demo2.py (rev 0)
+++ trunk/matplotlib/examples/pylab_examples/annotation_demo2.py 2008-10-30 13:18:07 UTC (rev 6353)
@@ -0,0 +1,151 @@
+
+from matplotlib.pyplot import figure, show
+from matplotlib.patches import Ellipse
+import numpy as np
+
+if 1:
+ fig = figure(1,figsize=(8,5))
+ ax = fig.add_subplot(111, autoscale_on=False, xlim=(-1,5), ylim=(-4,3))
+
+ t = np.arange(0.0, 5.0, 0.01)
+ s = np.cos(2*np.pi*t)
+ line, = ax.plot(t, s, lw=3, color='purple')
+
+ ax.annotate('arrowstyle', xy=(0, 1), xycoords='data',
+ xytext=(-50, 30), textcoords='offset points',
+ arrowprops=dict(arrowstyle="->")
+ )
+
+ ax.annotate('arc3', xy=(0.5, -1), xycoords='data',
+ xytext=(-30, -30), textcoords='offset points',
+ arrowprops=dict(arrowstyle="->",
+ connectionstyle="arc3,rad=.2")
+ )
+
+ ax.annotate('arc', xy=(1., 1), xycoords='data',
+ xytext=(-40, 30), textcoords='offset points',
+ arrowprops=dict(arrowstyle="->",
+ connectionstyle="arc,angleA=0,armA=30,rad=10"),
+ )
+
+ ax.annotate('arc', xy=(1.5, -1), xycoords='data',
+ xytext=(-40, -30), textcoords='offset points',
+ arrowprops=dict(arrowstyle="->",
+ connectionstyle="arc,angleA=0,armA=20,angleB=-90,armB=15,rad=7"),
+ )
+
+ ax.annotate('angle', xy=(2., 1), xycoords='data',
+ xytext=(-50, 30), textcoords='offset points',
+ arrowprops=dict(arrowstyle="->",
+ connectionstyle="angle,angleA=0,angleB=90,rad=10"),
+ )
+
+ ax.annotate('angle3', xy=(2.5, -1), xycoords='data',
+ xytext=(-50, -30), textcoords='offset points',
+ arrowprops=dict(arrowstyle="->",
+ connectionstyle="angle3,angleA=0,angleB=-90"),
+ )
+
+
+ ax.annotate('angle', xy=(3., 1), xycoords='data',
+ xytext=(-50, 30), textcoords='offset points',
+ bbox=dict(boxstyle="round", fc="0.8"),
+ arrowprops=dict(arrowstyle="->",
+ connectionstyle="angle,angleA=0,angleB=90,rad=10"),
+ )
+
+ ax.annotate('angle', xy=(3.5, -1), xycoords='data',
+ xytext=(-70, -60), textcoords='offset points',
+ size=20,
+ bbox=dict(boxstyle="round4,pad=.5", fc="0.8"),
+ arrowprops=dict(arrowstyle="->",
+ connectionstyle="angle,angleA=0,angleB=-90,rad=10"),
+ )
+
+ ax.annotate('angle', xy=(4., 1), xycoords='data',
+ xytext=(-50, 30), textcoords='offset points',
+ bbox=dict(boxstyle="round", fc="0.8"),
+ arrowprops=dict(arrowstyle="->",
+ shrinkA=0, shrinkB=10,
+ connectionstyle="angle,angleA=0,angleB=90,rad=10"),
+ )
+
+
+
+ fig.savefig('annotation_connection')
+
+
+if 1:
+ fig = figure(2)
+ fig.clf()
+ ax = fig.add_subplot(111, autoscale_on=False, xlim=(-1,5), ylim=(-5,3))
+
+ el = Ellipse((2, -1), 0.5, 0.5)
+ ax.add_patch(el)
+
+ ax.annotate('$->$', xy=(2., -1), xycoords='data',
+ xytext=(-150, -140), textcoords='offset points',
+ bbox=dict(boxstyle="round", fc="0.8"),
+ arrowprops=dict(arrowstyle="->",
+ patchB=el,
+ connectionstyle="angle,angleA=90,angleB=0,rad=10"),
+ )
+
+ ax.annotate('fancy', xy=(2., -1), xycoords='data',
+ xytext=(-100, 60), textcoords='offset points',
+ size=20,
+ #bbox=dict(boxstyle="round", fc="0.8"),
+ arrowprops=dict(arrowstyle="fancy",
+ fc="0.6", ec="none",
+ patchB=el,
+ connectionstyle="angle3,angleA=0,angleB=-90"),
+ )
+
+ ax.annotate('simple', xy=(2., -1), xycoords='data',
+ xytext=(100, 60), textcoords='offset points',
+ size=20,
+ #bbox=dict(boxstyle="round", fc="0.8"),
+ arrowprops=dict(arrowstyle="simple",
+ fc="0.6", ec="none",
+ patchB=el,
+ connectionstyle="arc3,rad=0.3"),
+ )
+
+ ax.annotate('wedge', xy=(2., -1), xycoords='data',
+ xytext=(-100, -100), textcoords='offset points',
+ size=20,
+ #bbox=dict(boxstyle="round", fc="0.8"),
+ arrowprops=dict(arrowstyle="wedge,tail_width=0.7",
+ fc="0.6", ec="none",
+ patchB=el,
+ connectionstyle="arc3,rad=-0.3"),
+ )
+
+
+ ann = ax.annotate('wedge', xy=(2., -1), xycoords='data',
+ xytext=(0, -45), textcoords='offset points',
+ size=20,
+ bbox=dict(boxstyle="round", fc=(1.0, 0.7, 0.7), ec=(1., .5, .5)),
+ arrowprops=dict(arrowstyle="wedge,tail_width=1.",
+ fc=(1.0, 0.7, 0.7), ec=(1., .5, .5),
+ patchA=None,
+ patchB=el,
+ relpos=(0.2, 0.8),
+ connectionstyle="arc3,rad=-0.1"),
+ )
+
+ ann = ax.annotate('wedge', xy=(2., -1), xycoords='data',
+ xytext=(35, 0), textcoords='offset points',
+ size=20, va="center",
+ bbox=dict(boxstyle="round", fc=(1.0, 0.7, 0.7), ec="none"),
+ arrowprops=dict(arrowstyle="wedge,tail_width=1.",
+ fc=(1.0, 0.7, 0.7), ec="none",
+ patchA=None,
+ patchB=el,
+ relpos=(0.2, 0.5),
+ )
+ )
+
+ fig.savefig('annotation_arrowstyle')
+
+show()
Added: trunk/matplotlib/lib/matplotlib/bezier.py
===================================================================
--- trunk/matplotlib/lib/matplotlib/bezier.py (rev 0)
+++ trunk/matplotlib/lib/matplotlib/bezier.py 2008-10-30 13:18:07 UTC (rev 6353)
@@ -0,0 +1,478 @@
+"""
+A module providing some utility functions regarding bezier path manipulation.
+"""
+
+
+import numpy as np
+from math import sqrt
+
+from matplotlib.path import Path
+
+from operator import xor
+
+
+# some functions
+
+def get_intersection(cx1, cy1, cos_t1, sin_t1,
+ cx2, cy2, cos_t2, sin_t2):
+ """ return a intersecting point between a line through (cx1, cy1)
+ and having angle t1 and a line through (cx2, cy2) and angle t2.
+ """
+
+ # line1 => sin_t1 * (x - cx1) - cos_t1 * (y - cy1) = 0.
+ # line1 => sin_t1 * x + cos_t1 * y = sin_t1*cx1 - cos_t1*cy1
+
+ line1_rhs = sin_t1 * cx1 - cos_t1 * cy1
+ line2_rhs = sin_t2 * cx2 - cos_t2 * cy2
+
+ # rhs matrix
+ a, b = sin_t1, -cos_t1
+ c, d = sin_t2, -cos_t2
+
+ ad_bc = a*d-b*c
+ if ad_bc == 0.:
+ raise ValueError("Given lines do not intersect")
+
+ #rhs_inverse
+ a_, b_ = d, -b
+ c_, d_ = -c, a
+ a_, b_, c_, d_ = [k / ad_bc for k in [a_, b_, c_, d_]]
+
+ x = a_* line1_rhs + b_ * line2_rhs
+ y = c_* line1_rhs + d_ * line2_rhs
+
+ return x, y
+
+
+
+def get_normal_points(cx, cy, cos_t, sin_t, length):
+ """
+ For a line passing through (*cx*, *cy*) and having a angle *t*,
+ return locations of the two points located along its perpendicular line at the distance of *length*.
+ """
+
+ if length == 0.:
+ return cx, cy, cx, cy
+
+ cos_t1, sin_t1 = sin_t, -cos_t
+ cos_t2, sin_t2 = -sin_t, cos_t
+
+ x1, y1 = length*cos_t1 + cx, length*sin_t1 + cy
+ x2, y2 = length*cos_t2 + cx, length*sin_t2 + cy
+
+ return x1, y1, x2, y2
+
+
+
+
+## BEZIER routines
+
+
+
+
+
+# subdividing bezier curve
+# http://www.cs.mtu.edu/~shene/COURSES/cs3621/NOTES/spline/Bezier/bezier-sub.html
+
+def _de_casteljau1(beta, t):
+ next_beta = beta[:-1] * (1-t) + beta[1:] * t
+ return next_beta
+
+def split_de_casteljau(beta, t):
+ """split a bezier segment defined by its controlpoints *beta*
+ into two separate segment divided at *t* and return their control points.
+
+ """
+ beta = np.asarray(beta)
+ beta_list = [beta]
+ while True:
+ beta = _de_casteljau1(beta, t)
+ beta_list.append(beta)
+ if len(beta) == 1:
+ break
+ left_beta = [beta[0] for beta in beta_list]
+ right_beta = [beta[-1] for beta in reversed(beta_list)]
+
+ return left_beta, right_beta
+
+
+
+
+
+
+
+def find_bezier_t_intersecting_with_closedpath(bezier_point_at_t, inside_closedpath,
+ t0=0., t1=1., tolerence=0.01):
+ """ Find a parameter t0 and t1 of the given bezier path which
+ bounds the intersecting points with a provided closed
+ path(*inside_closedpath*). Search starts from *t0* and *t1* and it
+ uses a simple bisecting algorithm therefore one of the end point
+ must be inside the path while the orther doesn't. The search stop
+ when |t0-t1| gets smaller than the given tolerence.
+ value for
+
+ - bezier_point_at_t : a function which returns x, y coordinates at *t*
+
+ - inside_closedpath : return True if the point is insed the path
+
+ """
+ # inside_closedpath : function
+
+ start = bezier_point_at_t(t0)
+ end = bezier_point_at_t(t1)
+
+ start_inside = inside_closedpath(start)
+ end_inside = inside_closedpath(end)
+
+ if not xor(start_inside, end_inside):
+ raise ValueError("the segment does not seemed to intersect with the path")
+
+ while 1:
+
+ # return if the distance is smaller than the tolerence
+ if (start[0]-end[0])**2 + (start[1]-end[1])**2 < tolerence**2:
+ return t0, t1
+
+ # calculate the middle point
+ middle_t = 0.5*(t0+t1)
+ middle = bezier_point_at_t(middle_t)
+ middle_inside = inside_closedpath(middle)
+
+ if xor(start_inside, middle_inside):
+ t1 = middle_t
+ end = middle
+ end_inside = middle_inside
+ else:
+ t0 = middle_t
+ start = middle
+ start_inside = middle_inside
+
+
+
+
+
+class BezierSegment:
+ """
+ A simple class of a 2-dimensional bezier segment
+ """
+
+ # Highrt order bezier lines can be supported by simplying adding
+ # correcponding values.
+ _binom_coeff = {1:np.array([1., 1.]),
+ 2:np.array([1., 2., 1.]),
+ 3:np.array([1., 3., 3., 1.])}
+
+ def __init__(self, control_points):
+ """
+ *control_points* : location of contol points. It needs have a
+ shpae of n * 2, where n is the order of the bezier line. 1<=
+ n <= 3 is supported.
+ """
+ _o = len(control_points)
+ self._orders = np.arange(_o)
+ _coeff = BezierSegment._binom_coeff[_o - 1]
+
+ _control_points = np.asarray(control_points)
+ xx = _control_points[:,0]
+ yy = _control_points[:,1]
+
+ self._px = xx * _coeff
+ self._py = yy * _coeff
+
+ def point_at_t(self, t):
+ "evaluate a point at t"
+ one_minus_t_powers = np.power(1.-t, self._orders)[::-1]
+ t_powers = np.power(t, self._orders)
+
+ tt = one_minus_t_powers * t_powers
+ _x = sum(tt * self._px)
+ _y = sum(tt * self._py)
+
+ return _x, _y
+
+
+def split_bezier_intersecting_with_closedpath(bezier,
+ inside_closedpath,
+ tolerence=0.01):
+
+ """
+ bezier : control points of the bezier segment
+ inside_closedpath : a function which returns true if the point is inside the path
+ """
+
+ bz = BezierSegment(bezier)
+ bezier_point_at_t = bz.point_at_t
+
+ t0, t1 = find_bezier_t_intersecting_with_closedpath(bezier_point_at_t,
+ inside_closedpath,
+ tolerence=tolerence)
+
+ _left, _right = split_de_casteljau(bezier, (t0+t1)/2.)
+ return _left, _right
+
+
+
+def find_r_to_boundary_of_closedpath(inside_closedpath, xy,
+ cos_t, sin_t,
+ rmin=0., rmax=1., tolerence=0.01):
+ """
+ Find a radius r (centered at *xy*) between *rmin* and *rmax* at
+ which it intersect with the path.
+
+ inside_closedpath : function
+ cx, cy : center
+ cos_t, sin_t : cosine and sine for the angle
+ rmin, rmax :
+ """
+
+ cx, cy = xy
+ def _f(r):
+ return cos_t*r + cx, sin_t*r + cy
+
+ find_bezier_t_intersecting_with_closedpath(_f, inside_closedpath,
+ t0=rmin, t1=rmax, tolerence=tolerence)
+
+
+
+## matplotlib specific
+
+def split_path_inout(path, inside, tolerence=0.01, reorder_inout=False):
+ """ divide a path into two segment at the point where inside(x, y)
+ becomes False.
+ """
+
+ path_iter = path.iter_segments()
+
+ ctl_points, command = path_iter.next()
+ begin_inside = inside(ctl_points[-2:]) # true if begin point is inside
+
+ bezier_path = None
+ ctl_points_old = ctl_points
+
+ concat = np.concatenate
+
+ iold=0
+ i = 1
+
+ for ctl_points, command in path_iter:
+ iold=i
+ i += len(ctl_points)/2
+ if inside(ctl_points[-2:]) != begin_inside:
+ bezier_path = concat([ctl_points_old[-2:], ctl_points])
+ break
+
+ ctl_points_old = ctl_points
+
+ if bezier_path is None:
+ raise ValueError("The path does not seem to intersect with the patch")
+
+ bp = zip(bezier_path[::2], bezier_path[1::2])
+ left, right = split_bezier_intersecting_with_closedpath(bp,
+ inside,
+ tolerence)
+ if len(left) == 2:
+ codes_left = [Path.LINETO]
+ codes_right = [Path.MOVETO, Path.LINETO]
+ elif len(left) == 3:
+ codes_left = [Path.CURVE3, Path.CURVE3]
+ codes_right = [Path.MOVETO, Path.CURVE3, Path.CURVE3]
+ elif len(left) == 4:
+ codes_left = [Path.CURVE4, Path.CURVE4, Path.CURVE4]
+ codes_right = [Path.MOVETO, Path.CURVE4, Path.CURVE4, Path.CURVE4]
+ else:
+ raise ValueError()
+
+ verts_left = left[1:]
+ verts_right = right[:]
+
+ #i += 1
+
+ if path.codes is None:
+ path_in = Path(concat([path.vertices[:i], verts_left]))
+ path_out = Path(concat([verts_right, path.vertices[i:]]))
+
+ else:
+ path_in = Path(concat([path.vertices[:iold], verts_left]),
+ concat([path.codes[:iold], codes_left]))
+
+ path_out = Path(concat([verts_right, path.vertices[i:]]),
+ concat([codes_right, path.codes[i:]]))
+
+ if reorder_inout and begin_inside == False:
+ path_in, path_out = path_out, path_in
+
+ return path_in, path_out
+
+
+
+
+
+def inside_circle(cx, cy, r):
+ r2 = r**2
+ def _f(xy):
+ x, y = xy
+ return (x-cx)**2 + (y-cy)**2 < r2
+ return _f
+
+
+
+# quadratic bezier lines
+
+def get_cos_sin(x0, y0, x1, y1):
+ dx, dy = x1-x0, y1-y0
+ d = (dx*dx + dy*dy)**.5
+ return dx/d, dy/d
+
+
+def get_parallels(bezier2, width):
+ """
+ Given the quadraitc bezier control points *bezier2*, returns
+ control points of quadrativ bezier lines roughly parralel to given
+ one separated by *width*.
+ """
+
+ # The parallel bezier lines constructed by following ways.
+ # c1 and c2 are contol points representing the begin and end of the bezier line.
+ # cm is the middle point
+ c1x, c1y = bezier2[0]
+ cmx, cmy = bezier2[1]
+ c2x, c2y = bezier2[2]
+
+ # t1 and t2 is the anlge between c1 and cm, cm, c2.
+ # They are also a angle of the tangential line of the path at c1 and c2
+ cos_t1, sin_t1 = get_cos_sin(c1x, c1y, cmx, cmy)
+ cos_t2, sin_t2 = get_cos_sin(cmx, cmy, c2x, c2y)
+
+ # find c1_left, c1_right which are located along the lines
+ # throught c1 and perpendicular to the tangential lines of the
+ # bezier path at a distance of width. Same thing for c2_left and
+ # c2_right with respect to c2.
+ c1x_left, c1y_left, c1x_right, c1y_right = \
+ get_normal_points(c1x, c1y, cos_t1, sin_t1, width)
+ c2x_left, c2y_left, c2x_right, c2y_right = \
+ get_normal_points(c2x, c2y, cos_t2, sin_t2, width)
+
+ # find cm_left which is the intersectng point of a line through
+ # c1_left with angle t1 and a line throught c2_left with angle
+ # t2. Same with cm_right.
+ cmx_left, cmy_left = get_intersection(c1x_left, c1y_left, cos_t1, sin_t1,
+ c2x_left, c2y_left, cos_t2, sin_t2)
+
+ cmx_right, cmy_right = get_intersection(c1x_right, c1y_right, cos_t1, sin_t1,
+ c2x_right, c2y_right, cos_t2, sin_t2)
+
+ # the parralel bezier lines are created with control points of
+ # [c1_left, cm_left, c2_left] and [c1_right, cm_right, c2_right]
+ path_left = [(c1x_left, c1y_left), (cmx_left, cmy_left), (c2x_left, c2y_left)]
+ path_right = [(c1x_right, c1y_right), (cmx_right, cmy_right), (c2x_right, c2y_right)]
+
+ return path_left, path_right
+
+
+
+def make_wedged_bezier2(bezier2, length, shrink_factor=0.5):
+ """
+ Being similar to get_parallels, returns
+ control points of two quadrativ bezier lines having a width roughly parralel to given
+ one separated by *width*.
+ """
+
+ xx1, yy1 = bezier2[2]
+ xx2, yy2 = bezier2[1]
+ xx3, yy3 = bezier2[0]
+
+ cx, cy = xx3, yy3
+ x0, y0 = xx2, yy2
+
+ dist = sqrt((x0-cx)**2 + (y0-cy)**2)
+ cos_t, sin_t = (x0-cx)/dist, (y0-cy)/dist,
+
+ x1, y1, x2, y2 = get_normal_points(cx, cy, cos_t, sin_t, length)
+
+ xx12, yy12 = (xx1+xx2)/2., (yy1+yy2)/2.,
+ xx23, yy23 = (xx2+xx3)/2., (yy2+yy3)/2.,
+
+ dist = sqrt((xx12-xx23)**2 + (yy12-yy23)**2)
+ cos_t, sin_t = (xx12-xx23)/dist, (yy12-yy23)/dist,
+
+ xm1, ym1, xm2, ym2 = get_normal_points(xx2, yy2, cos_t, sin_t, length*shrink_factor)
+
+ l_plus = [(x1, y1), (xm1, ym1), (xx1, yy1)]
+ l_minus = [(x2, y2), (xm2, ym2), (xx1, yy1)]
+
+ return l_plus, l_minus
+
+
+def find_control_points(c1x, c1y, mmx, mmy, c2x, c2y):
+ """ Find control points of the bezier line throught c1, mm, c2. We
+ simply assume that c1, mm, c2 which have parameteric value 0, 0.5, and 1.
+ """
+
+ cmx = .5 * (4*mmx - (c1x + c2x))
+ cmy = .5 * (4*mmy - (c1y + c2y))
+
+ return [(c1x, c1y), (cmx, cmy), (c2x, c2y)]
+
+
+def make_wedged_bezier2(bezier2, width, w1=1., wm=0.5, w2=0.):
+ """
+ Being similar to get_parallels, returns
+ control points of two quadrativ bezier lines having a width roughly parralel to given
+ one separated by *width*.
+ """
+
+ # c1, cm, c2
+ c1x, c1y = bezier2[0]
+ cmx, cmy = bezier2[1]
+ c3x, c3y = bezier2[2]
+
+
+ # t1 and t2 is the anlge between c1 and cm, cm, c3.
+ # They are also a angle of the tangential line of the path at c1 and c3
+ cos_t1, sin_t1 = get_cos_sin(c1x, c1y, cmx, cmy)
+ cos_t2, sin_t2 = get_cos_sin(cmx, cmy, c3x, c3y)
+
+ # find c1_left, c1_right which are located along the lines
+ # throught c1 and perpendicular to the tangential lines of the
+ # bezier path at a distance of width. Same thing for c3_left and
+ # c3_right with respect to c3.
+ c1x_left, c1y_left, c1x_right, c1y_right = \
+ get_normal_points(c1x, c1y, cos_t1, sin_t1, width*w1)
+ c3x_left, c3y_left, c3x_right, c3y_right = \
+ get_normal_points(c3x, c3y, cos_t2, sin_t2, width*w2)
+
+
+
+
+ # find c12, c23 and c123 which are middle points of c1-cm, cm-c3 and c12-c23
+ c12x, c12y = (c1x+cmx)*.5, (c1y+cmy)*.5
+ c23x, c23y = (cmx+c3x)*.5, (cmy+c3y)*.5
+ c123x, c123y = (c12x+c23x)*.5, (c12y+c23y)*.5
+
+ # tangential angle of c123 (angle between c12 and c23)
+ cos_t123, sin_t123 = get_cos_sin(c12x, c12y, c23x, c23y)
+
+ c123x_left, c123y_left, c123x_right, c123y_right = \
+ get_normal_points(c123x, c123y, cos_t123, sin_t123, width*wm)
+
+
+ path_left = find_control_points(c1x_left, c1y_left,
+ c123x_left, c123y_left,
+ c3x_left, c3y_left)
+ path_right = find_control_points(c1x_right, c1y_right,
+ c123x_right, c123y_right,
+ c3x_right, c3y_right)
+
+ return path_left, path_right
+
+
+
+
+if 0:
+ path = Path([(0, 0), (1, 0), (2, 2)],
+ [Path.MOVETO, Path.CURVE3, Path.CURVE3])
+ left, right = divide_path_inout(path, inside)
+ clf()
+ ax = gca()
+
+
Modified: trunk/matplotlib/lib/matplotlib/patches.py
===================================================================
--- trunk/matplotlib/lib/matplotlib/patches.py 2008-10-29 20:28:57 UTC (rev 6352)
+++ trunk/matplotlib/lib/matplotlib/patches.py 2008-10-30 13:18:07 UTC (rev 6353)
@@ -1475,7 +1475,153 @@
return path
+class Round4BoxTransmuter(BboxTransmuterBase):
+ """
+ A box with round edges.
+ """
+ def __init__(self, pad=0.3, rounding_size=None):
+ self.pad = pad
+ self.rounding_size = rounding_size
+ BboxTransmuterBase.__init__(self)
+
+ def transmute(self, x0, y0, width, height, mutation_size):
+
+ # padding
+ pad = mutation_size * self.pad
+
+ # roudning size. Use a half of the pad if not set.
+ if self.rounding_size:
+ dr = mutation_size * self.rounding_size
+ else:
+ dr = pad / 2.
+
+ width, height = width + 2.*pad - 2*dr, \
+ height + 2.*pad - 2*dr,
+
+
+ x0, y0 = x0-pad+dr, y0-pad+dr,
+ x1, y1 = x0+width, y0 + height
+
+
+ cp = [(x0, y0),
+ (x0+dr, y0-dr), (x1-dr, y0-dr), (x1, y0),
+ (x1+dr, y0+dr), (x1+dr, y1-dr), (x1, y1),
+ (x1-dr, y1+dr), (x0+dr, y1+dr), (x0, y1),
+ (x0-dr, y1-dr), (x0-dr, y0+dr), (x0, y0),
+ (x0, y0)]
+
+ com = [Path.MOVETO,
+ Path.CURVE4, Path.CURVE4, Path.CURVE4,
+ Path.CURVE4, Path.CURVE4, Path.CURVE4,
+ Path.CURVE4, Path.CURVE4, Path.CURVE4,
+ Path.CURVE4, Path.CURVE4, Path.CURVE4,
+ Path.CLOSEPOLY]
+
+ path = Path(cp, com)
+
+ return path
+
+
+
+
+class SawtoothBoxTransmuter(BboxTransmuterBase):
+ """
+ A sawtooth box.
+ """
+
+ def __init__(self, pad=0.3, tooth_size=None):
+ self.pad = pad
+ self.tooth_size = tooth_size
+ BboxTransmuterBase.__init__(self)
+
+ def _get_sawtooth_vertices(self, x0, y0, width, height, mutation_size):
+
+
+ # padding
+ pad = mutation_size * self.pad
+
+ # size of sawtooth
+ if self.tooth_size is None:
+ tooth_size = self.pad * .5 * mutation_size
+ else:
+ tooth_size = self.tooth_size * mutation_size
+
+ tooth_size2 = tooth_size / 2.
+ width, height = width + 2.*pad - tooth_size, \
+ height + 2.*pad - tooth_size,
+
+ # the sizes of the vertical and horizontal sawtooth are
+ # separately adjusted to fit the given box size.
+ dsx_n = int(round((width - tooth_size) / (tooth_size * 2))) * 2
+ dsx = (width - tooth_size) / dsx_n
+ dsy_n = int(round((height - tooth_size) / (tooth_size * 2))) * 2
+ dsy = (height - tooth_size) / dsy_n
+
+
+ x0, y0 = x0-pad+tooth_size2, y0-pad+tooth_size2
+ x1, y1 = x0+width, y0 + height
+
+
+ bottom_saw_x = [x0] + \
+ [x0 + tooth_size2 + dsx*.5* i for i in range(dsx_n*2)] + \
+ [x1 - tooth_size2]
+ bottom_saw_y = [y0] + \
+ [y0 - tooth_size2, y0, y0 + tooth_size2, y0] * dsx_n + \
+ [y0 - tooth_size2]
+
+ right_saw_x = [x1] + \
+ [x1 + tooth_size2, x1, x1 - tooth_size2, x1] * dsx_n + \
+ [x1 + tooth_size2]
+ right_saw_y = [y0] + \
+ [y0 + tooth_size2 + dsy*.5* i for i in range(dsy_n*2)] + \
+ [y1 - tooth_size2]
+
+ top_saw_x = [x1] + \
+ [x1 - tooth_size2 - dsx*.5* i for i in range(dsx_n*2)] + \
+ [x0 + tooth_size2]
+ top_saw_y = [y1] + \
+ [y1 + tooth_size2, y1, y1 - tooth_size2, y1] * dsx_n + \
+ [y1 + tooth_size2]
+
+ left_saw_x = [x0] + \
+ [x0 - tooth_size2, x0, x0 + tooth_size2, x0] * dsy_n + \
+ [x0 - tooth_size2]
+ left_saw_y = [y1] + \
+ [y1 - tooth_size2 - dsy*.5* i for i in range(dsy_n*2)] + \
+ [y0 + tooth_size2]
+
+ saw_vertices = zip(bottom_saw_x, bottom_saw_y) + \
+ zip(right_saw_x, right_saw_y) + \
+ zip(top_saw_x, top_saw_y) + \
+ zip(left_saw_x, left_saw_y) + \
+ [(bottom_saw_x[0], bottom_saw_y[0])]
+
+ return saw_vertices
+
+
+ def transmute(self, x0, y0, width, height, mutation_size):
+
+ saw_vertices = self._get_sawtooth_vertices(x0, y0, width, height, mutation_size)
+ path = Path(saw_vertices)
+ return path
+
+
+class RoundtoothBoxTransmuter(SawtoothBoxTransmuter):
+ """
+ A roundtooth(?) box.
+ """
+
+ def transmute(self, x0, y0, width, height, mutation_size):
+
+ saw_vertices = self._get_sawtooth_vertices(x0, y0, width, height, mutation_size)
+
+ cp = [Path.MOVETO] + ([Path.CURVE3, Path.CURVE3] * ((len(saw_vertices)-1)//2))
+ path = Path(saw_vertices, cp)
+
+ return path
+
+
def _list_available_boxstyles(transmuters):
""" a helper function of the :class:`FancyBboxPatch` to list the available
box styles. It inspects the arguments of the __init__ methods of
@@ -1520,6 +1666,9 @@
_fancy_bbox_transmuters = {"square":SquareBoxTransmuter,
"round":RoundBoxTransmuter,
+ "round4":Round4BoxTransmuter,
+ "sawtooth":SawtoothBoxTransmuter,
+ "roundtooth":RoundtoothBoxTransmuter,
}
def __str__(self):
@@ -1585,6 +1734,7 @@
__init__.__doc__ = cbook.dedent(__init__.__doc__) % kwdoc
del kwdoc
+ @classmethod
def list_available_boxstyles(cls):
return _list_available_boxstyles(cls._fancy_bbox_transmuters)
@@ -1758,3 +1908,1123 @@
def get_bbox(self):
return transforms.Bbox.from_bounds(self._x, self._y, self._width, self._height)
+
+
+
+
+from matplotlib.bezier import split_bezier_intersecting_with_closedpath
+from matplotlib.bezier import get_intersection, inside_circle, get_parallels
+from matplotlib.bezier import make_wedged_bezier2
+from matplotlib.bezier import split_path_inout, inside_circle
+
+class ConnectorBase(object):
+ """ The ConnectorClass is used to define a path between a two
+ points. This class is used in the FancyArrowPatch class. It
+ creates a path between point A and point B. When optional patch
+ objects (pathcA & patchB) are provided and if they enclose the
+ point A or B, the path is clipped to the boundary of the each
+ patch. Additionally the path can be shirnked by a fixed size
+ (given in points) with shrinkA and shrinkB.
+ """
+
+ class SimpleEvent:
+ def __init__(self, xy):
+ self.x, self.y = xy
+
+ def _clip(self, path, patchA, patchB):
+ """ Clip the path to the boundary of the patchA and patchB.
+ The starting point of the path needed to be inside of the
+ patchA and the end point inside the patch B. The contains
+ methods of each patch object is utilized to test if the point
+ is inside the path.
+ """
+
+ if patchA:
+ def insideA(xy_display):
+ #xy_display = patchA.get_data_transform().transform_point(xy_data)
+ xy_event = ConnectorBase.SimpleEvent(xy_display)
+ return patchA.contains(xy_event)[0]
+
+ try:
+ left, right = split_path_inout(path, insideA)
+ except ValueError:
+ right = path
+
+ path = right
+
+ if patchB:
+ def insideB(xy_display):
+ #xy_display = patchB.get_data_transform().transform_point(xy_data)
+ xy_event = ConnectorBase.SimpleEvent(xy_display)
+ return patchB.contains(xy_event)[0]
+
+ try:
+ left, right = split_path_inout(path, insideB)
+ except ValueError:
+ left = path
+
+ path = left
+
+ #ppp = patchB.get_patch_transform().transform_path(patchB.get_path())
+ #def insideB(xy_data):
+ # return ppp.contains_point(xy_data)
+ ##return patchB.contains(ConnectorBase.SimpleEvent(xy))[0]
+
+ return path
+
+
+ def _shrink(self, path, shrinkA, shrinkB):
+ """
+ Shrink the path by fixed size (in points) with shrinkA and shrinkB
+ """
+ if shrinkA:
+ x, y = path.vertices[0]
+ insideA = inside_circle(x, y, shrinkA)
+
+ left, right = split_path_inout(path, insideA)
+ path = right
+
+ if shrinkB:
+ x, y = path.vertices[-1]
+ insideB = inside_circle(x, y, shrinkB)
+
+ left, right = split_path_inout(path, insideB)
+ path = left
+
+ return path
+
+ def __call__(self, posA, posB,
+ shrinkA=2., shrinkB=2., patchA=None, patchB=None):
+
+ path = self.connect(posA, posB)
+
+ clipped_path = self._clip(path, patchA, patchB)
+ shrinked_path = self._shrink(clipped_path, shrinkA, shrinkB)
+
+ return shrinked_path
+
+
+class Arc3Connector(ConnectorBase):
+ """ Creates a simple quadratic bezier curve between two
+ points. The curve is created so that the middle contol points (C1)
+ is located at the same distance from the start (C0) and end
+ points(C2) and the distance of the C1 to the line connecting C0-C2
+ is *rad* times the distance of C0-C2.
+ """
+ def __init__(self, rad=0.):
+ self.rad = rad
+
+ def connect(self, posA, posB):
+ x1, y1 = posA
+ x2, y2 = posB
+ x12, y12 = (x1 + x2)/2., (y1 + y2)/2.
+ dx, dy = x2 - x1, y2 - y1
+
+ f = self.rad
+
+ cx, cy = x12 + f*dy, y12 - f*dx
+
+ vertices = [(x1, y1),
+ (cx, cy),
+ (x2, y2)]
+ codes = [Path.MOVETO,
+ Path.CURVE3,
+ Path.CURVE3]
+
+ return Path(vertices, codes)
+
+
+class Angle3Connector(ConnectorBase):
+ """ Creates a simple quadratic bezier curve between two
+ points. The middle control points is placed at the intersecting
+ point of two lines which crosses the start (or end) point
+ and has a angle of angleA (or angleB).
+ """
+ def __init__(self, angleA=90, angleB=0):
+ self.angleA = angleA
+ self.angleB = angleB
+
+ def connect(self, posA, posB):
+ x1, y1 = posA
+ x2, y2 = posB
+
+ cosA, sinA = math.cos(self.angleA/180.*math.pi),\
+ math.sin(self.angleA/180.*math.pi),
+ cosB, sinB = math.cos(self.angleB/180.*math.pi),\
+ math.sin(self.angleB/180.*math.pi),
+
+ cx, cy = get_intersection(x1, y1, cosA, sinA,
+ x2, y2, cosB, sinB)
+
+ vertices = [(x1, y1), (cx, cy), (x2, y2)]
+ codes = [Path.MOVETO, Path.CURVE3, Path.CURVE3]
+
+ return Path(vertices, codes)
+
+
+class AngleConnector(ConnectorBase):
+ """ Creates a picewise continuous quadratic bezier path between
+ two points. The path has a one passing-through point placed at the
+ intersecting point of two lines which crosses the start (or end)
+ point and has a angle of angleA (or angleB). The connecting edges are
+ rounded with *rad*.
+ """
+
+ def __init__(self, angleA=90, angleB=0, rad=0.):
+ self.angleA = angleA
+ self.angleB = angleB
+
+ self.rad = rad
+
+ def connect(self, posA, posB):
+ x1, y1 = posA
+ x2, y2 = posB
+
+ cosA, sinA = math.cos(self.angleA/180.*math.pi),\
+ math.sin(self.angleA/180.*math.pi),
+ cosB, sinB = math.cos(self.angleB/180.*math.pi),\
+ -math.sin(self.angleB/180.*math.pi),
+
+ cx, cy = get_intersection(x1, y1, cosA, sinA,
+ x2, y2, cosB, sinB)
+
+ vertices = [(x1, y1)]
+ codes = [Path.MOVETO]
+
+ if self.rad == 0.:
+ vertices.append((cx, cy))
+ codes.append(Path.LINETO)
+ else:
+ vertices.extend([(cx - self.rad * cosA, cy - self.rad * sinA),
+ (cx, cy),
+ (cx + self.rad * cosB, cy + self.rad * sinB)])
+ codes.extend([Path.LINETO, Path.CURVE3, Path.CURVE3])
+
+ vertices.append((x2, y2))
+ codes.append(Path.LINETO)
+
+ return Path(vertices, codes)
+
+
+
+class ArcConnector(ConnectorBase):
+ """ Creates a picewise continuous quadratic bezier path between
+ two points. The path can have two passing-through points, a point
+ placed at the distance of armA and angle of angleA from point A,
+ another point with respect to point B. The edges are rounded with
+ *rad*.
+ """
+
+ def __init__(self, angleA=0, angleB=0, armA=None, armB=None, rad=0.):
+ self.angleA = angleA
+ self.angleB = angleB
+ self.armA = armA
+ self.armB = armB
+
+ self.rad = rad
+
+ def connect(self, posA, posB):
+ x1, y1 = posA
+ x2, y2 = posB
+
+ vertices = [(x1, y1)]
+ rounded = []
+ codes = [Path.MOVETO]
+
+ if self.armA:
+ cosA = math.cos(self.angleA/180.*math.pi)
+ sinA = math.sin(self.angleA/180.*math.pi)
+ #x_armA, y_armB
+ d = self.armA - self.rad
+ rounded.append((x1 + d*cosA, y1 + d*sinA))
+ d = self.armA
+ rounded.append((x1 + d*cosA, y1 + d*sinA))
+
+ if self.armB:
+ cosB = math.cos(self.angleB/180.*math.pi)
+ sinB = math.sin(self.angleB/180.*math.pi)
+ x_armB, y_armB = x2 + self.armB*cosB, y2 + self.armB*sinB
+
+ if rounded:
+ xp, yp = rounded[-1]
+ dx, dy = x_armB - xp, y_armB - yp
+ dd = (dx*dx + dy*dy)**.5
+
+ rounded.append((xp + self.rad*dx/dd, yp + self.rad*dy/dd))
+ vertices.extend(rounded)
+ codes.extend([Path.LINETO,
+ Path.CURVE3,
+ Path.CURVE3])
+ else:
+ xp, yp = vertices[-1]
+ dx, dy = x_armB - xp, y_armB - yp
+ dd = (dx*dx + dy*dy)**.5
+
+ d = dd - self.rad
+ rounded = [(xp + d*dx/dd, yp + d*dy/dd),
+ (x_armB, y_armB)]
+
+ if rounded:
+ xp, yp = rounded[-1]
+ dx, dy = x2 - xp, y2 - yp
+ dd = (dx*dx + dy*dy)**.5
+
+ rounded.append((xp + self.rad*dx/dd, yp + self.rad*dy/dd))
+ vertices.extend(rounded)
+ codes.extend([Path.LINETO,
+ Path.CURVE3,
+ Path.CURVE3])
+
+ vertices.append((x2, y2))
+ codes.append(Path.LINETO)
+
+ return Path(vertices, codes)
+
+
+
+
+class ArrowTransmuterBase(object):
+ """
+ Arrow Transmuter Base class
+
+ ArrowTransmuterBase and its derivatives are used to make a fancy
+ arrow around a given path. The __call__ method returns a path
+ (which will be used to create a PathPatch instance) and a boolean
+ value indicating the path is open therefore is not fillable. This
+ class is not an artist and actual drawing of the fancy arrow is
+ done by the FancyArrowPatch class.
+
+ """
+
+ # The derived classes are required to be able to be initialized
+ # w/o arguments, i.e., all its argument (except self) must have
+ # the default values.
+
+ def __init__(self):
+ super(ArrowTransmuterBase, self).__init__()
+
+ @staticmethod
+ def ensure_quadratic_bezier(path):
+ """ Some ArrowTransmuter class only wokrs with a simple
+ quaratic bezier curve (created with Arc3Connetion or
+ Angle3Connector). This static method is to check if the
+ provided path is a simple quadratic bezier curve and returns
+ its control points if true.
+ """
+ segments = list(path.iter_segments())
+ assert len(segments) == 2
+
+ assert segments[0][1] == Path.MOVETO
+ assert segments[1][1] == Path.CURVE3
+
+ return list(segments[0][0]) + list(segments[1][0])
+
+
+ def transmute(self, path, mutation_size, linewidth):
+ """
+ The transmute method is a very core of the ArrowTransmuter
+ class and must be overriden in the subclasses. It receives the
+ path object along which the arrow will be drawn, and the
+ mutation_size, with which the amount arrow head and etc. will
+ be scaled. It returns a Path instance. The linewidth may be
+ used to adjust the the path so that it does not pass beyond
+ the given points.
+ """
+
+ raise NotImplementedError('Derived must override')
+
+
+
+ def __call__(self, path, mutation_size, linewidth,
+ aspect_ratio=1.):
+ """
+ The __call__ method is a thin wrapper around the transmute method
+ and take care of the aspect ratio.
+ """
+
+ if aspect_ratio is not None:
+ # Squeeze the given height by the aspect_ratio
+
+ vertices, codes = path.vertices[:], path.codes[:]
+ # Squeeze the height
+ vertices[:,1] = vertices[:,1] / aspect_ratio
+ path_shrinked = Path(vertices, codes)
+ # call transmute method with squeezed height.
+ path_mutated, closed = self.transmute(path_shrinked, linewidth,
+ mutation_size)
+ vertices, codes = path_mutate.vertices, path_mutate.codes
+ # Restore the height
+ vertices[:,1] = vertices[:,1] * aspect_ratio
+ return Path(vertices, codes), closed
+ else:
+ return self.transmute(path, mutation_size, linewidth)
+
+
+
+class CurveArrowTransmuter(ArrowTransmuterBase):
+ """
+ A simple arrow which will work with any path instance. The
+ returned path is simply concatenation of the original path + at
+ most two paths representing the arrow at the begin point and the
+ at the end point. The returned path is not closed and only meant
+ to be stroked.
+ """
+
+ def __init__(self, beginarrow=None, endarrow=None,
+ head_length=.2, head_width=.1):
+ """ The arrows are drawn if *beginarrow* and/or *endarrow* are
+ true. *head_length* and *head_width* determines the size of
+ the arrow relative to the *mutation scale*.
+ """
+ self.beginarrow, self.endarrow = beginarrow, endarrow
+ self.head_length, self.head_width = \
+ head_length, head_width
+ super(CurveArrowTransmuter, self).__init__()
+
+
+ def _get_pad_projected(self, x0, y0, x1, y1, linewidth):
+ # when no arrow head is drawn
+
+ dx, dy = x0 - x1, y0 - y1
+ cp_distance = math.sqrt(dx**2 + dy**2)
+
+ # padx_projected, pady_projected : amount of pad to account
+ # projection of the wedge
+ padx_projected = (.5*linewidth)
+ pady_projected = (.5*linewidth)
+
+ # apply pad for projected edge
+ ddx = padx_projected * dx / cp_distance
+ ddy = pady_projected * dy / cp_distance
+
+ return ddx, ddy
+
+ def _get_arrow_wedge(self, x0, y0, x1, y1,
+ head_dist, cos_t, sin_t, linewidth
+ ):
+ """ Return the paths for arrow heads. Since arrow lines are
+ drawn with capstyle=projected, The arrow is goes beyond the
+ desired point. This method also returns the amount of the path
+ to be shrinked so that it does not overshoot.
+ """
+
+ # arrow from x0, y0 to x1, y1
+
+
+ dx, dy = x0 - x1, y0 - y1
+ cp_distance = math.sqrt(dx**2 + dy**2)
+
+ # padx_projected, pady_projected : amount of pad for account
+ # the overshooting of the projection of the wedge
+ padx_projected = (.5*linewidth / cos_t)
+ pady_projected = (.5*linewidth / sin_t)
+
+ # apply pad for projected edge
+ ddx = padx_projected * dx / cp_distance
+ ddy = pady_projected * dy / cp_distance
+
+ # offset for arrow wedge
+ dx, dy = dx / cp_distance * head_dist, dy / cp_distance * head_dist
+
+ dx1, dy1 = cos_t * dx + sin_t * dy, -sin_t * dx + cos_t * dy
+ dx2, dy2 = cos_t * dx - sin_t * dy, sin_t * dx + cos_t * dy
+
+ vertices_arrow = [(x1+ddx+dx1, y1+ddy+dy1),
+ (x1+ddx, y1++ddy),
+ (x1+ddx+dx2, y1+ddy+dy2)]
+ codes_arrow = [Path.MOVETO,
+ Path.LINETO,
+ Path.LINETO]
+
+ return vertices_arrow, codes_arrow, ddx, ddy
+
+
+ def transmute(self, path, mutation_size, linewidth):
+
+ head_length, head_width = self.head_length * mutation_size, \
+ self.head_width * mutation_size
+ head_dist = math.sqrt(head_length**2 + head_width**2)
+ cos_t, sin_t = head_length / head_dist, head_width / head_dist
+
+
+ # begin arrow
+ x0, y0 = path.vertices[0]
+ x1, y1 = path.vertices[1]
+
+ if self.beginarrow:
+ verticesA, codesA, ddxA, ddyA = \
+ self._get_arrow_wedge(x1, y1, x0, y0,
+ head_dist, cos_t, sin_t,
+ linewidth)
+ else:
+ verticesA, codesA = [], []
+ #ddxA, ddyA = self._get_pad_projected(x1, y1, x0, y0, linewidth)
+ ddxA, ddyA = 0., 0., #self._get_pad_projected(x1, y1, x0, y0, linewidth)
+
+ # end arrow
+ x2, y2 = path.vertices[-2]
+ x3, y3 = path.vertices[-1]
+
+ if self.endarrow:
+ verticesB, codesB, ddxB, ddyB = \
+ self._get_arrow_wedge(x2, y2, x3, y3,
+ head_dist, cos_t, sin_t,
+ linewidth)
+ else:
+ verticesB, codesB = [], []
+ ddxB, ddyB = 0., 0. #self._get_pad_projected(x2, y2, x3, y3, linewidth)
+
+
+ # this simple code will not work if ddx, ddy is greater than
+ # separation bettern vertices.
+ vertices = np.concatenate([verticesA + [(x0+ddxA, y0+ddyA)],
+ path.vertices[1:-1],
+ [(x3+ddxB, y3+ddyB)] + verticesB])
+ codes = np.concatenate([codesA,
+ path.codes,
+ codesB])
+
+ p = Path(vertices, codes)
+
+ return p, False
+
+
+class CurveArrowATransmuter(CurveArrowTransmuter):
+ """
+ A CurveArrowTransmuter with arrow at begin point. This class is
+ only meant to be used to define the arrowstyle and users may
+ simply use the original CurveArrowTransmuter class when necesary.
+ """
+
+ def __init__(self, head_length=.4, head_width=.2):
+ super(CurveArrowATransmuter, self).__init__( \
+ beginarrow=True, endarrow=False,
+ head_length=head_length, head_width=head_width )
+
+
+class CurveArrowBTransmuter(CurveArrowTransmuter):
+ """
+ A CurveArrowTransmuter with arrow at end point. This class is
+ only meant to be used to define the arrowstyle and users may
+ simply use the original CurveArrowTransmuter class when necesary.
+ """
+
+ def __init__(self, head_length=.4, head_width=.2):
+ super(CurveArrowBTransmuter, self).__init__( \
+ beginarrow=False, endarrow=True,
+ head_length=head_length, head_width=head_width )
+
+
+class CurveArrowABTransmuter(CurveArrowTransmuter):
+ """
+ A CurveArrowTransmuter with arrows at both begin and end
+ points. This class is only meant to be used to define the
+ arrowstyle and users may simply use the original
+ CurveArrowTransmuter class when necesary.
+ """
+
+ def __init__(self, head_length=.4, head_width=.2):
+ super(CurveArrowABTransmuter, self).__init__( \
+ beginarrow=True, endarrow=True,
+ head_length=head_length, head_width=head_width )
+
+
+
+class SimpleArrowTransmuter(ArrowTransmuterBase):
+ """
+ A simple arrow. Only works with a quadratic bezier curve.
+ """
+
+ def __init__(self, head_length=.5, head_width=.5, tail_width=.2):
+ self.head_length, self.head_width, self.tail_width = \
+ head_length, head_width, tail_width
+ super(SimpleArrowTransmuter, self).__init__()
+
+ def transmute(self, path, mutation_size, linewidth):
+
+ x0, y0, x1, y1, x2, y2 = self.ensure_quadratic_bezier(path)
+
+ # divide the path into a head and a tail
+ head_length = self.head_length * mutation_size
+ in_f = inside_circle(x2, y2, head_length)
+ arrow_path = [(x0, y0), (x1, y1), (x2, y2)]
+ arrow_out, arrow_in = \
+ split_bezier_intersecting_with_closedpath(arrow_path,
+ in_f,
+ tolerence=0.01)
+
+ # head
+ head_width = self.head_width * mutation_size
+ head_l, head_r = make_wedged_bezier2(arrow_in, head_width/2.,
+ wm=.8)
+
+
+
+ # tail
+ tail_width = self.tail_width * mutation_size
+ tail_left, tail_right = get_parallels(arrow_out, tail_width/2.)
+
+ head_right, head_left = head_r, head_l
+ patch_path = [(Path.MOVETO, tail_right[0]),
+ (Path.CURVE3, tail_right[1]),
+ (Path.CURVE3, tail_right[2]),
+ (Path.LINETO, head_right[0]),
+ (Path.CURVE3, head_right[1]),
+ (Path.CURVE3, head_right[2]),
+ (Path.CURVE3, head_left[1]),
+ (Path.CURVE3, head_left[0]),
+ (Path.LINETO, tail_left[2]),
+ (Path.CURVE3, tail_left[1]),
+ (Path.CURVE3, tail_left[0]),
+ (Path.LINETO, tail_right[0]),
+ (Path.CLOSEPOLY, tail_right[0]),
+ ]
+ path = Path([p for c, p in patch_path], [c for c, p in patch_path])
+
+ return path, True
+
+
+class FancyArrowTransmuter(ArrowTransmuterBase):
+ """
+ A fancy arrow. Only works with a quadratic bezier curve.
+ """
+
+ def __init__(self, head_length=.4, head_width=.4, tail_width=.4):
+ self.head_length, self.head_width, self.tail_width = \
+ head_length, head_width, tail_width
+ super(FancyArrowTransmuter, self).__init__()
+
+ def transmute(self, path, mutation_size, linewidth):
+
+ x0, y0, x1, y1, x2, y2 = self.ensure_quadratic_bezier(path)
+
+ # divide the path into a head and a tail
+ head_length = self.head_length * mutation_size
+ arrow_path = [(x0, y0), (x1, y1), (x2, y2)]
+
+ # path for head
+ in_f = inside_circle(x2, y2, head_length)
+ path_out, path_in = \
+ split_bezier_intersecting_with_closedpath(arrow_path,
+ in_f,
+ tolerence=0.01)
+ path_head = path_in
+
+ # path for head
+ in_f = inside_circle(x2, y2, head_length*.8)
+ path_out, path_in = \
+ split_bezier_intersecting_with_closedpath(arrow_path,
+ in_f,
+ tolerence=0.01)
+ path_tail = path_out
+
+
+ # head
+ head_width = self.head_width * mutation_size
+ head_l, head_r = make_wedged_bezier2(path_head, head_width/2.,
+ wm=.6)
+
+ # tail
+ tail_width = self.tail_width * mutation_size
+ tail_left, tail_right = make_wedged_bezier2(path_tail,
+ tail_width*.5,
+ w1=1., wm=0.6, w2=0.3)
+
+ # path for head
+ in_f = inside_circle(x0, y0, tail_width*.3)
+ path_in, path_out = \
+ split_bezier_intersecting_with_closedpath(arrow_path,
+ in_f,
+ tolerence=0.01)
+ tail_start = path_in[-1]
+
+ head_right, head_left = head_r, head_l
+ patch_path = [(Path.MOVETO, tail_start),
+ (Path.LINETO, tail_right[0]),
+ (Path.CURVE3, tail_right[1]),
+ (Path.CURVE3, tail_right[2]),
+ (Path.LINETO, head_right[0]),
+ (Path.CURVE3, head_right[1]),
+ (Path.CURVE3, head_right[2]),
+ (Path.CURVE3, head_left[1]),
+ (Path.CURVE3, head_left[0]),
+ (Path.LINETO, tail_left[2]),
+ (Path.CURVE3, tail_left[1]),
+ (Path.CURVE3, tail_left[0]),
+ (Path.LINETO, tail_start),
+ (Path.CLOSEPOLY, tail_start),
+ ]
+ patch_path2 = [(Path.MOVETO, tail_right[0]),
+ (Path.CURVE3, tail_right[1]),
+ (Path.CURVE3, tail_right[2]),
+ (Path.LINETO, head_right[0]),
+ (Path.CURVE3, head_right[1]),
+ (Path.CURVE3, head_right[2]),
+ (Path.CURVE3, head_left[1]),
+ (Path.CURVE3, head_left[0]),
+ (Path.LINETO, tail_left[2]),
+ (Path.CURVE3, tail_left[1]),
+ (Path.CURVE3, tail_left[0]),
+ (Path.CURVE3, tail_start),
+ (Path.CURVE3, tail_right[0]),
+ (Path.CLOSEPOLY, tail_right[0]),
+ ]
+ path = Path([p for c, p in patch_path], [c for c, p in patch_path])
+
+ return path, True
+
+
+
+
+
+class WedgeArrowTransmuter(ArrowTransmuterBase):
+ """
+ Wedge(?) shape. Only wokrs with a quadratic bezier curve. The
+ begin point has a width of the tail_width and the end point has a
+ width of 0. At the middle, the width is shrink_factor*tail_width.
+ """
+
+ def __init__(self, tail_width=.3, shrink_factor=0.5):
+ self.tail_width = tail_width
+ self.shrink_factor = shrink_factor
+ super(WedgeArrowTransmuter, self).__init__()
+
+
+ def transmute(self, path, mutation_size, linewidth):
+
+ x0, y0, x1, y1, x2, y2 = self.ensure_quadratic_bezier(path)
+
+ arrow_path = [(x0, y0), (x1, y1), (x2, y2)]
+ b_plus, b_minus = make_wedged_bezier2(arrow_path,
+ self.tail_width * mutation_size / 2.,
+ wm=self.shrink_factor)
+
+
+ patch_path = [(Path.MOVETO, b_plus[0]),
+ (Path.CURVE3, b_plus[1]),
+ (Path.CURVE3, b_plus[2]),
+ (Path.LINETO, b_minus[2]),
+ (Path.CURVE3, b_minus[1]),
+ (Path.CURVE3, b_minus[0]),
+ (Path.CLOSEPOLY, b_minus[0]),
+ ]
+ path = Path([p for c, p in patch_path], [c for c, p in patch_path])
+
+ return path, True
+
+
+
+
+def _list_available_connectionstyles(connectors):
+ """ a helper function of the FancyArrowPatch to list the available
+ connection styles. It inspects the arguments of the __init__ methods of
+ each classes and report them
+ """
+ import inspect
+ s = []
+ for name, cls in connectors.items():
+ args, varargs, varkw, defaults = inspect.getargspec(cls.__init__)
+ args_string = ["%s=%s" % (argname, str(argdefault)) \
+ for argname, argdefault in zip(args[1:], defaults)]
+ s.append(",".join([name]+args_string))
+ s.sort()
+ return s
+
+def _list_available_arrowstyles(transmuters):
+ """ a helper function of the FancyArrowPatch to list the available
+ arrow styles. It inspects the arguments of the __init__ methods of
+ each classes and report them
+ """
+ import inspect
+ s = []
+ for name, cls in transmuters.items():
+ args, varargs, varkw, defaults = inspect.getargspec(cls.__init__)
+ args_string = ["%s=%s" % (argname, str(argdefault)) \
+ for argname, argdefault in zip(args[1:], defaults)]
+ s.append(",".join([name]+args_string))
+ s.sort()
+ return s
+
+
+
+class FancyArrowPatch(Patch):
+ """
+ Draw a fancy arrow along a path.
+
+ The "arrowstyle" argument determins what kind of
+ arrow will be drawn. In other words, it selects the
+ ArrowTransmuter class to use, and sets optional attributes. A
+ custom ArrowTransmuter can be used with arrow_transmuter argument
+ (should be an instance, not a class). mutation_scale determines
+ the overall size of the mutation (by which I mean the
+ transformation of the path to the fancy arrow) and the
+ mutation_aspect determines the aspect-ratio of the mutation.
+
+ """
+
+ _fancy_arrow_transmuters = {"simple":SimpleArrowTransmuter,
+ "fancy":FancyArrowTransmuter,
+ "wedge":WedgeArrowTransmuter,
+ "-":CurveArrowTransmuter,
+ "->":CurveArrowBTransmuter,
+ "<-":CurveArrowATransmuter,
+ "<->":CurveArrowABTransmuter,
+ }
+
+ _connectors = {"arc3":Arc3Connector,
+ "arc":ArcConnector,
+ "angle":AngleConnector,
+ "angle3":Angle3Connector,
+ }
+
+ def __str__(self):
+ return self.__class__.__name__ \
+ + "FancyArrowPatch(%g,%g,%g,%g,%g,%g)" % tuple(self._q_bezier)
+
+ def __init__(self, posA=None, posB=None,
+ path=None,
+ arrowstyle="simple",
+ arrow_transmuter=None,
+ connectionstyle="arc3",
+ connector=None,
+ patchA=None,
+ patchB=None,
+ shrinkA=2.,
+ shrinkB=2.,
+ mutation_scale=1.,
+ mutation_aspect=None,
+ **kwargs):
+ """
+ If *posA* and *posB* is given, a path connecting two point are
+ created according to the connectionstyle. The path will be
+ clipped with *patchA* and *patchB* and further shirnked by
+ *shrinkA* and *shrinkB*. An arrow is drawn along this
+ resulting path using the *arrowstyle* parameter. If *path*
+ provided, an arrow is drawn along this path and *patchA*,
+ *patchB*, *shrinkA*, and *shrinkB* are ignored.
+
+ The *connectionstyle* describes how *posA* and *posB* are
+ connected. It should be one of the available connectionstyle
+ names, with optional comma-separated attributes. Following
+ connection styles are available.
+
+ %(AvailableConnectorstyles)s
+
+ The connectionstyle name can be "custom", in which case the
+ *connector* needs to be set, which should be an instance
+ of ArrowTransmuterBase (or its derived).
+
+
+ The *arrowstyle* describes how the fancy arrow will be drawn. It
+ should be one of the available arrowstyle names, with optional
+ comma-separated attributes. These attributes are meant to be
+ scaled with the *mutation_scale*. Following arrow styles are
+ available.
+
+ %(AvailableArrowstyles)s
+
+ The arrowstyle name can be "custom", in which case the
+ arrow_transmuter needs to be set, which should be an instance
+ of ArrowTransmuterBase (or its derived).
+
+ *mutation_scale* : a value with which attributes of arrowstyle
+ (e.g., head_length) will be scaled. default=1.
+
+ *mutation_aspect* : The height of the rectangle will be
+ squeezed by this value before the mutation and the mutated
+ box will be stretched by the inverse of it. default=None.
+
+ Valid kwargs are:
+ %(Patch)s
+ """
+
+ if posA is not None and posB is not None and path is None:
+ self._posA_posB = [posA, posB]
+
+ if connectionstyle == "custom":
+ if connector is None:
+ raise ValueError("connector argument is needed with custom connectionstyle")
+ self.set_connector(connector)
+ else:
+ if connectionstyle is None:
+ connectionstyle = "arc3"
+ self.set_connectionstyle(connectionstyle)
+
+ elif posA is None and posB is None and path is not None:
+ self._posA_posB = None
+ self._connetors = None
+ else:
+ raise ValueError("either posA and po...
[truncated message content] |