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] |