|
| 1 | +""" |
| 2 | +=================================== |
| 3 | +Angle annotations on bracket arrows |
| 4 | +=================================== |
| 5 | +
|
| 6 | +This example shows how to add angle annotations to bracket arrow styles |
| 7 | +created using `.FancyArrowPatch`. Angles are annotated using |
| 8 | +``AngleAnnotation`` from the example |
| 9 | +:doc:`/gallery/text_labels_and_annotations/angle_annotation`. |
| 10 | +Additional `.FancyArrowPatch` arrows are added to show the directions of |
| 11 | +*angleA* and *angleB*. |
| 12 | +""" |
| 13 | + |
| 14 | +############################################################################# |
| 15 | +# The ``AngleAnnotation`` class is copied from the |
| 16 | +# :doc:`/gallery/text_labels_and_annotations/angle_annotation` example. |
| 17 | + |
| 18 | +importmatplotlib.pyplotasplt |
| 19 | +importnumpyasnp |
| 20 | +frommatplotlib.patchesimportArc,FancyArrowPatch |
| 21 | +frommatplotlib.transformsimportBbox,IdentityTransform,TransformedBbox |
| 22 | + |
| 23 | + |
| 24 | +classAngleAnnotation(Arc): |
| 25 | +""" |
| 26 | + Draws an arc between two vectors which appears circular in display space. |
| 27 | + """ |
| 28 | +def__init__(self,xy,p1,p2,size=75,unit="points",ax=None, |
| 29 | +text="",textposition="inside",text_kw=None,**kwargs): |
| 30 | +""" |
| 31 | + Parameters |
| 32 | + ---------- |
| 33 | + xy, p1, p2 : tuple or array of two floats |
| 34 | + Center position and two points. Angle annotation is drawn between |
| 35 | + the two vectors connecting *p1* and *p2* with *xy*, respectively. |
| 36 | + Units are data coordinates. |
| 37 | +
|
| 38 | + size : float |
| 39 | + Diameter of the angle annotation in units specified by *unit*. |
| 40 | +
|
| 41 | + unit : str |
| 42 | + One of the following strings to specify the unit of *size*: |
| 43 | +
|
| 44 | + * "pixels": pixels |
| 45 | + * "points": points, use points instead of pixels to not have a |
| 46 | + dependence on the DPI |
| 47 | + * "axes width", "axes height": relative units of Axes width, height |
| 48 | + * "axes min", "axes max": minimum or maximum of relative Axes |
| 49 | + width, height |
| 50 | +
|
| 51 | + ax : `matplotlib.axes.Axes` |
| 52 | + The Axes to add the angle annotation to. |
| 53 | +
|
| 54 | + text : str |
| 55 | + The text to mark the angle with. |
| 56 | +
|
| 57 | + textposition : {"inside", "outside", "edge"} |
| 58 | + Whether to show the text in- or outside the arc. "edge" can be used |
| 59 | + for custom positions anchored at the arc's edge. |
| 60 | +
|
| 61 | + text_kw : dict |
| 62 | + Dictionary of arguments passed to the Annotation. |
| 63 | +
|
| 64 | + **kwargs |
| 65 | + Further parameters are passed to `matplotlib.patches.Arc`. Use this |
| 66 | + to specify, color, linewidth etc. of the arc. |
| 67 | +
|
| 68 | + """ |
| 69 | +self.ax=axorplt.gca() |
| 70 | +self._xydata=xy# in data coordinates |
| 71 | +self.vec1=p1 |
| 72 | +self.vec2=p2 |
| 73 | +self.size=size |
| 74 | +self.unit=unit |
| 75 | +self.textposition=textposition |
| 76 | + |
| 77 | +super().__init__(self._xydata,size,size,angle=0.0, |
| 78 | +theta1=self.theta1,theta2=self.theta2,**kwargs) |
| 79 | + |
| 80 | +self.set_transform(IdentityTransform()) |
| 81 | +self.ax.add_patch(self) |
| 82 | + |
| 83 | +self.kw=dict(ha="center",va="center", |
| 84 | +xycoords=IdentityTransform(), |
| 85 | +xytext=(0,0),textcoords="offset points", |
| 86 | +annotation_clip=True) |
| 87 | +self.kw.update(text_kwor {}) |
| 88 | +self.text=ax.annotate(text,xy=self._center,**self.kw) |
| 89 | + |
| 90 | +defget_size(self): |
| 91 | +factor=1. |
| 92 | +ifself.unit=="points": |
| 93 | +factor=self.ax.figure.dpi/72. |
| 94 | +elifself.unit[:4]=="axes": |
| 95 | +b=TransformedBbox(Bbox.unit(),self.ax.transAxes) |
| 96 | +dic= {"max":max(b.width,b.height), |
| 97 | +"min":min(b.width,b.height), |
| 98 | +"width":b.width,"height":b.height} |
| 99 | +factor=dic[self.unit[5:]] |
| 100 | +returnself.size*factor |
| 101 | + |
| 102 | +defset_size(self,size): |
| 103 | +self.size=size |
| 104 | + |
| 105 | +defget_center_in_pixels(self): |
| 106 | +"""return center in pixels""" |
| 107 | +returnself.ax.transData.transform(self._xydata) |
| 108 | + |
| 109 | +defset_center(self,xy): |
| 110 | +"""set center in data coordinates""" |
| 111 | +self._xydata=xy |
| 112 | + |
| 113 | +defget_theta(self,vec): |
| 114 | +vec_in_pixels=self.ax.transData.transform(vec)-self._center |
| 115 | +returnnp.rad2deg(np.arctan2(vec_in_pixels[1],vec_in_pixels[0])) |
| 116 | + |
| 117 | +defget_theta1(self): |
| 118 | +returnself.get_theta(self.vec1) |
| 119 | + |
| 120 | +defget_theta2(self): |
| 121 | +returnself.get_theta(self.vec2) |
| 122 | + |
| 123 | +defset_theta(self,angle): |
| 124 | +pass |
| 125 | + |
| 126 | +# Redefine attributes of the Arc to always give values in pixel space |
| 127 | +_center=property(get_center_in_pixels,set_center) |
| 128 | +theta1=property(get_theta1,set_theta) |
| 129 | +theta2=property(get_theta2,set_theta) |
| 130 | +width=property(get_size,set_size) |
| 131 | +height=property(get_size,set_size) |
| 132 | + |
| 133 | +# The following two methods are needed to update the text position. |
| 134 | +defdraw(self,renderer): |
| 135 | +self.update_text() |
| 136 | +super().draw(renderer) |
| 137 | + |
| 138 | +defupdate_text(self): |
| 139 | +c=self._center |
| 140 | +s=self.get_size() |
| 141 | +angle_span= (self.theta2-self.theta1)%360 |
| 142 | +angle=np.deg2rad(self.theta1+angle_span/2) |
| 143 | +r=s/2 |
| 144 | +ifself.textposition=="inside": |
| 145 | +r=s/np.interp(angle_span, [60,90,135,180], |
| 146 | + [3.3,3.5,3.8,4]) |
| 147 | +self.text.xy=c+r*np.array([np.cos(angle),np.sin(angle)]) |
| 148 | +ifself.textposition=="outside": |
| 149 | +defR90(a,r,w,h): |
| 150 | +ifa<np.arctan(h/2/(r+w/2)): |
| 151 | +returnnp.sqrt((r+w/2)**2+ (np.tan(a)*(r+w/2))**2) |
| 152 | +else: |
| 153 | +c=np.sqrt((w/2)**2+(h/2)**2) |
| 154 | +T=np.arcsin(c*np.cos(np.pi/2-a+np.arcsin(h/2/c))/r) |
| 155 | +xy=r*np.array([np.cos(a+T),np.sin(a+T)]) |
| 156 | +xy+=np.array([w/2,h/2]) |
| 157 | +returnnp.sqrt(np.sum(xy**2)) |
| 158 | + |
| 159 | +defR(a,r,w,h): |
| 160 | +aa= (a% (np.pi/4))*((a% (np.pi/2))<=np.pi/4)+ \ |
| 161 | + (np.pi/4- (a% (np.pi/4)))*((a% (np.pi/2))>=np.pi/4) |
| 162 | +returnR90(aa,r,*[w,h][::int(np.sign(np.cos(2*a)))]) |
| 163 | + |
| 164 | +bbox=self.text.get_window_extent() |
| 165 | +X=R(angle,r,bbox.width,bbox.height) |
| 166 | +trans=self.ax.figure.dpi_scale_trans.inverted() |
| 167 | +offs=trans.transform(((X-s/2),0))[0]*72 |
| 168 | +self.text.set_position([offs*np.cos(angle),offs*np.sin(angle)]) |
| 169 | + |
| 170 | + |
| 171 | +############################################################################# |
| 172 | +# The plot is generatd with the following code. |
| 173 | + |
| 174 | +defget_point_of_rotated_vertical(origin,line_length,degrees): |
| 175 | +"""Return xy coordinates of the vertical line end rotated by degrees.""" |
| 176 | +rad=np.deg2rad(-degrees) |
| 177 | +return [origin[0]+line_length*np.sin(rad), |
| 178 | +origin[1]+line_length*np.cos(rad)] |
| 179 | + |
| 180 | + |
| 181 | +fig,ax=plt.subplots(figsize=(8,7)) |
| 182 | +ax.set(xlim=(0,6),ylim=(-1,4)) |
| 183 | +ax.set_title('Angle annotations on bracket arrows') |
| 184 | + |
| 185 | +fori,stylenameinenumerate(["]-[","|-|"]): |
| 186 | +forj,angleinenumerate([-40,60]): |
| 187 | +y=2*i+j |
| 188 | +arrow_centers= [(1,y), (5,y)] |
| 189 | +vlines= [[c[0],y+0.5]forcinarrow_centers] |
| 190 | +widths="widthA=1.5,widthB=1.5" |
| 191 | +arrowstyle=f"{stylename},{widths},angleA={angle},angleB={-angle}" |
| 192 | +patch=FancyArrowPatch(arrow_centers[0],arrow_centers[1], |
| 193 | +arrowstyle=arrowstyle,mutation_scale=25) |
| 194 | +ax.add_patch(patch) |
| 195 | +ax.text(3,y+0.05,arrowstyle.replace(f"{widths},",""), |
| 196 | +verticalalignment="bottom",horizontalalignment="center") |
| 197 | +ax.vlines([i[0]foriinvlines], [y,y], [i[1]foriinvlines], |
| 198 | +linestyles="--",color="C0") |
| 199 | +# Get the coordinates for the drawn patches at A and B |
| 200 | +patch_top_coords= [ |
| 201 | +get_point_of_rotated_vertical(arrow_centers[0],0.5,angle), |
| 202 | +get_point_of_rotated_vertical(arrow_centers[1],0.5,-angle) |
| 203 | + ] |
| 204 | +# Create points for annotating A and B with AngleAnnotation |
| 205 | +# Points include the top of the vline and patch_top_coords |
| 206 | +pointsA= [(1,y+0.5),patch_top_coords[0]] |
| 207 | +pointsB= [patch_top_coords[1], (5,y+0.5)] |
| 208 | +# Define the directions for the arrows for AngleAnnotation |
| 209 | +arrow_angles= [0.5,-0.5] |
| 210 | +# Reverse the points and arrow_angles when the angle is negative |
| 211 | +ifangle<0: |
| 212 | +pointsA.reverse() |
| 213 | +pointsB.reverse() |
| 214 | +arrow_angles.reverse() |
| 215 | +# Add AngleAnnotation and arrows to show angle directions |
| 216 | +data=zip(arrow_centers, [pointsA,pointsB],vlines,arrow_angles, |
| 217 | +patch_top_coords) |
| 218 | +forcenter,points,vline,arrow_angle,patch_topindata: |
| 219 | +am=AngleAnnotation(center,points[0],points[1],ax=ax, |
| 220 | +size=0.25,unit="axes min",text=str(-angle)) |
| 221 | +arrowstyle="Simple, tail_width=0.5, head_width=4, head_length=8" |
| 222 | +kw=dict(arrowstyle=arrowstyle,color="C0") |
| 223 | +arrow=FancyArrowPatch(vline,patch_top, |
| 224 | +connectionstyle=f"arc3,rad={arrow_angle}", |
| 225 | +**kw) |
| 226 | +ax.add_patch(arrow) |
| 227 | + |
| 228 | + |
| 229 | +############################################################################# |
| 230 | +# |
| 231 | +# .. admonition:: References |
| 232 | +# |
| 233 | +# The use of the following functions, methods, classes and modules is shown |
| 234 | +# in this example: |
| 235 | +# |
| 236 | +# - `matplotlib.patches.ArrowStyle` |