proteusPy.angle_annotation
===========================
Scale invariant angle label
This example shows how to create a scale invariant angle annotation. It is
often useful to mark angles between lines or inside shapes with a circular arc.
While Matplotlib provides an ~.patches.Arc, an inherent problem when directly
using it for such purposes is that an arc being circular in data space is not
necessarily circular in display space. Also, the arc's radius is often best
defined in a coordinate system which is independent of the actual data
coordinates - at least if you want to be able to freely zoom into your plot
without the annotation growing to infinity.
This calls for a solution where the arc's center is defined in data space, but
its radius in a physical unit like points or pixels, or as a ratio of the Axes
dimension. The following AngleAnnotation class provides such solution.
The example below serves two purposes:
- It provides a ready-to-use solution for the problem of easily drawing angles in graphs.
- It shows how to subclass a Matplotlib artist to enhance its functionality, as
well as giving a hands-on example on how to use Matplotlib's :doc:
transform system </tutorials/advanced/transforms_tutorial>.
If mainly interested in the former, you may copy the below class and jump to
the :ref:angle-annotation-usage section.
1""" 2=========================== 3Scale invariant angle label 4=========================== 5 6This example shows how to create a scale invariant angle annotation. It is 7often useful to mark angles between lines or inside shapes with a circular arc. 8While Matplotlib provides an `~.patches.Arc`, an inherent problem when directly 9using it for such purposes is that an arc being circular in data space is not 10necessarily circular in display space. Also, the arc's radius is often best 11defined in a coordinate system which is independent of the actual data 12coordinates - at least if you want to be able to freely zoom into your plot 13without the annotation growing to infinity. 14 15This calls for a solution where the arc's center is defined in data space, but 16its radius in a physical unit like points or pixels, or as a ratio of the Axes 17dimension. The following ``AngleAnnotation`` class provides such solution. 18 19The example below serves two purposes: 20 21* It provides a ready-to-use solution for the problem of easily drawing angles 22 in graphs. 23* It shows how to subclass a Matplotlib artist to enhance its functionality, as 24 well as giving a hands-on example on how to use Matplotlib's :doc:`transform 25 system </tutorials/advanced/transforms_tutorial>`. 26 27If mainly interested in the former, you may copy the below class and jump to 28the :ref:`angle-annotation-usage` section. 29""" 30 31######################################################################### 32# AngleAnnotation class 33# ~~~~~~~~~~~~~~~~~~~~~ 34# The essential idea here is to subclass `~.patches.Arc` and set its transform 35# to the `~.transforms.IdentityTransform`, making the parameters of the arc 36# defined in pixel space. 37# We then override the ``Arc``'s attributes ``_center``, ``theta1``, 38# ``theta2``, ``width`` and ``height`` and make them properties, coupling to 39# internal methods that calculate the respective parameters each time the 40# attribute is accessed and thereby ensuring that the arc in pixel space stays 41# synchronized with the input points and size. 42# For example, each time the arc's drawing method would query its ``_center`` 43# attribute, instead of receiving the same number all over again, it will 44# instead receive the result of the ``get_center_in_pixels`` method we defined 45# in the subclass. This method transforms the center in data coordinates to 46# pixels via the Axes transform ``ax.transData``. The size and the angles are 47# calculated in a similar fashion, such that the arc changes its shape 48# automatically when e.g. zooming or panning interactively. 49# 50# The functionality of this class allows to annotate the arc with a text. This 51# text is a `~.text.Annotation` stored in an attribute ``text``. Since the 52# arc's position and radius are defined only at draw time, we need to update 53# the text's position accordingly. This is done by reimplementing the ``Arc``'s 54# ``draw()`` method to let it call an updating method for the text. 55# 56# The arc and the text will be added to the provided Axes at instantiation: it 57# is hence not strictly necessary to keep a reference to it. 58 59import matplotlib.pyplot as plt 60import numpy as np 61from matplotlib.patches import Arc 62from matplotlib.transforms import Bbox, IdentityTransform, TransformedBbox 63 64 65class AngleAnnotation(Arc): 66 """ 67 Draws an arc between two vectors which appears circular in display space. 68 """ 69 70 def __init__( 71 self, 72 xy, 73 p1, 74 p2, 75 size=75, 76 unit="points", 77 ax=None, 78 text="", 79 textposition="inside", 80 text_kw=None, 81 **kwargs, 82 ): 83 """ 84 Parameters 85 ---------- 86 xy, p1, p2 : tuple or array of two floats 87 Center position and two points. Angle annotation is drawn between 88 the two vectors connecting *p1* and *p2* with *xy*, respectively. 89 Units are data coordinates. 90 91 size : float 92 Diameter of the angle annotation in units specified by *unit*. 93 94 unit : str 95 One of the following strings to specify the unit of *size*: 96 97 * "pixels": pixels 98 * "points": points, use points instead of pixels to not have a 99 dependence on the DPI 100 * "axes width", "axes height": relative units of Axes width, height 101 * "axes min", "axes max": minimum or maximum of relative Axes 102 width, height 103 104 ax : `matplotlib.axes.Axes` 105 The Axes to add the angle annotation to. 106 107 text : str 108 The text to mark the angle with. 109 110 textposition : {"inside", "outside", "edge"} 111 Whether to show the text in- or outside the arc. "edge" can be used 112 for custom positions anchored at the arc's edge. 113 114 text_kw : dict 115 Dictionary of arguments passed to the Annotation. 116 117 **kwargs 118 Further parameters are passed to `matplotlib.patches.Arc`. Use this 119 to specify, color, linewidth etc. of the arc. 120 121 """ 122 self.ax = ax or plt.gca() 123 self._xydata = xy # in data coordinates 124 self.vec1 = p1 125 self.vec2 = p2 126 self.size = size 127 self.unit = unit 128 self.textposition = textposition 129 130 super().__init__( 131 self._xydata, size, size, angle=0.0, theta1=self.theta1, theta2=self.theta2, **kwargs 132 ) 133 134 self.set_transform(IdentityTransform()) 135 self.ax.add_patch(self) 136 137 self.kw = dict( 138 ha="center", 139 va="center", 140 xycoords=IdentityTransform(), 141 xytext=(0, 0), 142 textcoords="offset points", 143 annotation_clip=True, 144 ) 145 self.kw.update(text_kw or {}) 146 self.text = ax.annotate(text, xy=self._center, **self.kw) 147 148 def get_size(self): 149 factor = 1.0 150 if self.unit == "points": 151 factor = self.ax.figure.dpi / 72.0 152 elif self.unit[:4] == "axes": 153 b = TransformedBbox(Bbox.unit(), self.ax.transAxes) 154 dic = { 155 "max": max(b.width, b.height), 156 "min": min(b.width, b.height), 157 "width": b.width, 158 "height": b.height, 159 } 160 factor = dic[self.unit[5:]] 161 return self.size * factor 162 163 def set_size(self, size): 164 self.size = size 165 166 def get_center_in_pixels(self): 167 """return center in pixels""" 168 return self.ax.transData.transform(self._xydata) 169 170 def set_center(self, xy): 171 """set center in data coordinates""" 172 self._xydata = xy 173 174 def get_theta(self, vec): 175 vec_in_pixels = self.ax.transData.transform(vec) - self._center 176 return np.rad2deg(np.arctan2(vec_in_pixels[1], vec_in_pixels[0])) 177 178 def get_theta1(self): 179 return self.get_theta(self.vec1) 180 181 def get_theta2(self): 182 return self.get_theta(self.vec2) 183 184 def set_theta(self, angle): 185 pass 186 187 # Redefine attributes of the Arc to always give values in pixel space 188 _center = property(get_center_in_pixels, set_center) 189 theta1 = property(get_theta1, set_theta) 190 theta2 = property(get_theta2, set_theta) 191 width = property(get_size, set_size) 192 height = property(get_size, set_size) 193 194 # The following two methods are needed to update the text position. 195 def draw(self, renderer): 196 self.update_text() 197 super().draw(renderer) 198 199 def update_text(self): 200 c = self._center 201 s = self.get_size() 202 angle_span = (self.theta2 - self.theta1) % 360 203 angle = np.deg2rad(self.theta1 + angle_span / 2) 204 r = s / 2 205 if self.textposition == "inside": 206 r = s / np.interp(angle_span, [60, 90, 135, 180], [3.3, 3.5, 3.8, 4]) 207 self.text.xy = c + r * np.array([np.cos(angle), np.sin(angle)]) 208 if self.textposition == "outside": 209 210 def R90(a, r, w, h): 211 if a < np.arctan(h / 2 / (r + w / 2)): 212 return np.sqrt((r + w / 2) ** 2 + (np.tan(a) * (r + w / 2)) ** 2) 213 else: 214 c = np.sqrt((w / 2) ** 2 + (h / 2) ** 2) 215 T = np.arcsin(c * np.cos(np.pi / 2 - a + np.arcsin(h / 2 / c)) / r) 216 xy = r * np.array([np.cos(a + T), np.sin(a + T)]) 217 xy += np.array([w / 2, h / 2]) 218 return np.sqrt(np.sum(xy**2)) 219 220 def R(a, r, w, h): 221 aa = (a % (np.pi / 4)) * ((a % (np.pi / 2)) <= np.pi / 4) + ( 222 np.pi / 4 - (a % (np.pi / 4)) 223 ) * ((a % (np.pi / 2)) >= np.pi / 4) 224 return R90(aa, r, *[w, h][:: int(np.sign(np.cos(2 * a)))]) 225 226 bbox = self.text.get_window_extent() 227 X = R(angle, r, bbox.width, bbox.height) 228 trans = self.ax.figure.dpi_scale_trans.inverted() 229 offs = trans.transform(((X - s / 2), 0))[0] * 72 # !!! 230 self.text.set_position([offs * np.cos(angle), offs * np.sin(angle)]) 231 232 233# Helper function to draw angle easily. 234def plot_angle(ax, pos, angle, length=0.95, acol="C0", **kwargs): 235 vec2 = np.array([np.cos(np.deg2rad(angle)), np.sin(np.deg2rad(angle))]) 236 xy = np.c_[[length, 0], [0, 0], vec2 * length].T + np.array(pos) 237 ax.plot(*xy.T, color=acol) 238 return AngleAnnotation(pos, xy[0], xy[2], ax=ax, **kwargs) 239 240 241######################################################################### 242# ``AngleLabel`` options 243# ~~~~~~~~~~~~~~~~~~~~~~ 244# 245# The *textposition* and *unit* keyword arguments may be used to modify the 246# location of the text label, as shown below: 247 248 249""" 250fig, (ax1, ax2) = plt.subplots(nrows=2, sharex=True) 251fig.suptitle("AngleLabel keyword arguments") 252fig.canvas.draw() # Need to draw the figure to define renderer 253 254# Showcase different text positions. 255ax1.margins(y=0.4) 256ax1.set_title("textposition") 257kw = dict(size=75, unit="points", text=r"$60°$") 258 259am6 = plot_angle(ax1, (2.0, 0), 60, textposition="inside", **kw) 260am7 = plot_angle(ax1, (3.5, 0), 60, textposition="outside", **kw) 261am8 = plot_angle(ax1, (5.0, 0), 60, textposition="edge", 262 text_kw=dict(bbox=dict(boxstyle="round", fc="w")), **kw) 263am9 = plot_angle(ax1, (6.5, 0), 60, textposition="edge", 264 text_kw=dict(xytext=(30, 20), arrowprops=dict(arrowstyle="->", 265 connectionstyle="arc3,rad=-0.2")), **kw) 266 267for x, text in zip([2.0, 3.5, 5.0, 6.5], ['"inside"', '"outside"', '"edge"', 268 '"edge", custom arrow']): 269 ax1.annotate(text, xy=(x, 0), xycoords=ax1.get_xaxis_transform(), 270 bbox=dict(boxstyle="round", fc="w"), ha="left", fontsize=8, 271 annotation_clip=True) 272 273# Showcase different size units. The effect of this can best be observed 274# by interactively changing the figure size 275ax2.margins(y=0.4) 276ax2.set_title("unit") 277kw = dict(text=r"$60°$", textposition="outside") 278 279am10 = plot_angle(ax2, (2.0, 0), 60, size=50, unit="pixels", **kw) 280am11 = plot_angle(ax2, (3.5, 0), 60, size=50, unit="points", **kw) 281am12 = plot_angle(ax2, (5.0, 0), 60, size=0.25, unit="axes min", **kw) 282am13 = plot_angle(ax2, (6.5, 0), 60, size=0.25, unit="axes max", **kw) 283 284for x, text in zip([2.0, 3.5, 5.0, 6.5], ['"pixels"', '"points"', 285 '"axes min"', '"axes max"']): 286 ax2.annotate(text, xy=(x, 0), xycoords=ax2.get_xaxis_transform(), 287 bbox=dict(boxstyle="round", fc="w"), ha="left", fontsize=8, 288 annotation_clip=True) 289 290plt.show() 291 292""" 293 294 295############################################################################# 296# 297# .. admonition:: References 298# 299# The use of the following functions, methods, classes and modules is shown 300# in this example: 301# 302# - `matplotlib.patches.Arc` 303# - `matplotlib.axes.Axes.annotate` / `matplotlib.pyplot.annotate` 304# - `matplotlib.text.Annotation` 305# - `matplotlib.transforms.IdentityTransform` 306# - `matplotlib.transforms.TransformedBbox` 307# - `matplotlib.transforms.Bbox`
66class AngleAnnotation(Arc): 67 """ 68 Draws an arc between two vectors which appears circular in display space. 69 """ 70 71 def __init__( 72 self, 73 xy, 74 p1, 75 p2, 76 size=75, 77 unit="points", 78 ax=None, 79 text="", 80 textposition="inside", 81 text_kw=None, 82 **kwargs, 83 ): 84 """ 85 Parameters 86 ---------- 87 xy, p1, p2 : tuple or array of two floats 88 Center position and two points. Angle annotation is drawn between 89 the two vectors connecting *p1* and *p2* with *xy*, respectively. 90 Units are data coordinates. 91 92 size : float 93 Diameter of the angle annotation in units specified by *unit*. 94 95 unit : str 96 One of the following strings to specify the unit of *size*: 97 98 * "pixels": pixels 99 * "points": points, use points instead of pixels to not have a 100 dependence on the DPI 101 * "axes width", "axes height": relative units of Axes width, height 102 * "axes min", "axes max": minimum or maximum of relative Axes 103 width, height 104 105 ax : `matplotlib.axes.Axes` 106 The Axes to add the angle annotation to. 107 108 text : str 109 The text to mark the angle with. 110 111 textposition : {"inside", "outside", "edge"} 112 Whether to show the text in- or outside the arc. "edge" can be used 113 for custom positions anchored at the arc's edge. 114 115 text_kw : dict 116 Dictionary of arguments passed to the Annotation. 117 118 **kwargs 119 Further parameters are passed to `matplotlib.patches.Arc`. Use this 120 to specify, color, linewidth etc. of the arc. 121 122 """ 123 self.ax = ax or plt.gca() 124 self._xydata = xy # in data coordinates 125 self.vec1 = p1 126 self.vec2 = p2 127 self.size = size 128 self.unit = unit 129 self.textposition = textposition 130 131 super().__init__( 132 self._xydata, size, size, angle=0.0, theta1=self.theta1, theta2=self.theta2, **kwargs 133 ) 134 135 self.set_transform(IdentityTransform()) 136 self.ax.add_patch(self) 137 138 self.kw = dict( 139 ha="center", 140 va="center", 141 xycoords=IdentityTransform(), 142 xytext=(0, 0), 143 textcoords="offset points", 144 annotation_clip=True, 145 ) 146 self.kw.update(text_kw or {}) 147 self.text = ax.annotate(text, xy=self._center, **self.kw) 148 149 def get_size(self): 150 factor = 1.0 151 if self.unit == "points": 152 factor = self.ax.figure.dpi / 72.0 153 elif self.unit[:4] == "axes": 154 b = TransformedBbox(Bbox.unit(), self.ax.transAxes) 155 dic = { 156 "max": max(b.width, b.height), 157 "min": min(b.width, b.height), 158 "width": b.width, 159 "height": b.height, 160 } 161 factor = dic[self.unit[5:]] 162 return self.size * factor 163 164 def set_size(self, size): 165 self.size = size 166 167 def get_center_in_pixels(self): 168 """return center in pixels""" 169 return self.ax.transData.transform(self._xydata) 170 171 def set_center(self, xy): 172 """set center in data coordinates""" 173 self._xydata = xy 174 175 def get_theta(self, vec): 176 vec_in_pixels = self.ax.transData.transform(vec) - self._center 177 return np.rad2deg(np.arctan2(vec_in_pixels[1], vec_in_pixels[0])) 178 179 def get_theta1(self): 180 return self.get_theta(self.vec1) 181 182 def get_theta2(self): 183 return self.get_theta(self.vec2) 184 185 def set_theta(self, angle): 186 pass 187 188 # Redefine attributes of the Arc to always give values in pixel space 189 _center = property(get_center_in_pixels, set_center) 190 theta1 = property(get_theta1, set_theta) 191 theta2 = property(get_theta2, set_theta) 192 width = property(get_size, set_size) 193 height = property(get_size, set_size) 194 195 # The following two methods are needed to update the text position. 196 def draw(self, renderer): 197 self.update_text() 198 super().draw(renderer) 199 200 def update_text(self): 201 c = self._center 202 s = self.get_size() 203 angle_span = (self.theta2 - self.theta1) % 360 204 angle = np.deg2rad(self.theta1 + angle_span / 2) 205 r = s / 2 206 if self.textposition == "inside": 207 r = s / np.interp(angle_span, [60, 90, 135, 180], [3.3, 3.5, 3.8, 4]) 208 self.text.xy = c + r * np.array([np.cos(angle), np.sin(angle)]) 209 if self.textposition == "outside": 210 211 def R90(a, r, w, h): 212 if a < np.arctan(h / 2 / (r + w / 2)): 213 return np.sqrt((r + w / 2) ** 2 + (np.tan(a) * (r + w / 2)) ** 2) 214 else: 215 c = np.sqrt((w / 2) ** 2 + (h / 2) ** 2) 216 T = np.arcsin(c * np.cos(np.pi / 2 - a + np.arcsin(h / 2 / c)) / r) 217 xy = r * np.array([np.cos(a + T), np.sin(a + T)]) 218 xy += np.array([w / 2, h / 2]) 219 return np.sqrt(np.sum(xy**2)) 220 221 def R(a, r, w, h): 222 aa = (a % (np.pi / 4)) * ((a % (np.pi / 2)) <= np.pi / 4) + ( 223 np.pi / 4 - (a % (np.pi / 4)) 224 ) * ((a % (np.pi / 2)) >= np.pi / 4) 225 return R90(aa, r, *[w, h][:: int(np.sign(np.cos(2 * a)))]) 226 227 bbox = self.text.get_window_extent() 228 X = R(angle, r, bbox.width, bbox.height) 229 trans = self.ax.figure.dpi_scale_trans.inverted() 230 offs = trans.transform(((X - s / 2), 0))[0] * 72 # !!! 231 self.text.set_position([offs * np.cos(angle), offs * np.sin(angle)])
Draws an arc between two vectors which appears circular in display space.
71 def __init__( 72 self, 73 xy, 74 p1, 75 p2, 76 size=75, 77 unit="points", 78 ax=None, 79 text="", 80 textposition="inside", 81 text_kw=None, 82 **kwargs, 83 ): 84 """ 85 Parameters 86 ---------- 87 xy, p1, p2 : tuple or array of two floats 88 Center position and two points. Angle annotation is drawn between 89 the two vectors connecting *p1* and *p2* with *xy*, respectively. 90 Units are data coordinates. 91 92 size : float 93 Diameter of the angle annotation in units specified by *unit*. 94 95 unit : str 96 One of the following strings to specify the unit of *size*: 97 98 * "pixels": pixels 99 * "points": points, use points instead of pixels to not have a 100 dependence on the DPI 101 * "axes width", "axes height": relative units of Axes width, height 102 * "axes min", "axes max": minimum or maximum of relative Axes 103 width, height 104 105 ax : `matplotlib.axes.Axes` 106 The Axes to add the angle annotation to. 107 108 text : str 109 The text to mark the angle with. 110 111 textposition : {"inside", "outside", "edge"} 112 Whether to show the text in- or outside the arc. "edge" can be used 113 for custom positions anchored at the arc's edge. 114 115 text_kw : dict 116 Dictionary of arguments passed to the Annotation. 117 118 **kwargs 119 Further parameters are passed to `matplotlib.patches.Arc`. Use this 120 to specify, color, linewidth etc. of the arc. 121 122 """ 123 self.ax = ax or plt.gca() 124 self._xydata = xy # in data coordinates 125 self.vec1 = p1 126 self.vec2 = p2 127 self.size = size 128 self.unit = unit 129 self.textposition = textposition 130 131 super().__init__( 132 self._xydata, size, size, angle=0.0, theta1=self.theta1, theta2=self.theta2, **kwargs 133 ) 134 135 self.set_transform(IdentityTransform()) 136 self.ax.add_patch(self) 137 138 self.kw = dict( 139 ha="center", 140 va="center", 141 xycoords=IdentityTransform(), 142 xytext=(0, 0), 143 textcoords="offset points", 144 annotation_clip=True, 145 ) 146 self.kw.update(text_kw or {}) 147 self.text = ax.annotate(text, xy=self._center, **self.kw)
Parameters
xy, p1, p2 : tuple or array of two floats Center position and two points. Angle annotation is drawn between the two vectors connecting p1 and p2 with xy, respectively. Units are data coordinates.
size : float Diameter of the angle annotation in units specified by unit.
unit : str One of the following strings to specify the unit of size:
* "pixels": pixels
* "points": points, use points instead of pixels to not have a
dependence on the DPI
* "axes width", "axes height": relative units of Axes width, height
* "axes min", "axes max": minimum or maximum of relative Axes
width, height
ax : matplotlib.axes.Axes
The Axes to add the angle annotation to.
text : str The text to mark the angle with.
textposition : {"inside", "outside", "edge"} Whether to show the text in- or outside the arc. "edge" can be used for custom positions anchored at the arc's edge.
text_kw : dict Dictionary of arguments passed to the Annotation.
**kwargs
Further parameters are passed to matplotlib.patches.Arc. Use this
to specify, color, linewidth etc. of the arc.
149 def get_size(self): 150 factor = 1.0 151 if self.unit == "points": 152 factor = self.ax.figure.dpi / 72.0 153 elif self.unit[:4] == "axes": 154 b = TransformedBbox(Bbox.unit(), self.ax.transAxes) 155 dic = { 156 "max": max(b.width, b.height), 157 "min": min(b.width, b.height), 158 "width": b.width, 159 "height": b.height, 160 } 161 factor = dic[self.unit[5:]] 162 return self.size * factor
167 def get_center_in_pixels(self): 168 """return center in pixels""" 169 return self.ax.transData.transform(self._xydata)
return center in pixels
149 def get_size(self): 150 factor = 1.0 151 if self.unit == "points": 152 factor = self.ax.figure.dpi / 72.0 153 elif self.unit[:4] == "axes": 154 b = TransformedBbox(Bbox.unit(), self.ax.transAxes) 155 dic = { 156 "max": max(b.width, b.height), 157 "min": min(b.width, b.height), 158 "width": b.width, 159 "height": b.height, 160 } 161 factor = dic[self.unit[5:]] 162 return self.size * factor
149 def get_size(self): 150 factor = 1.0 151 if self.unit == "points": 152 factor = self.ax.figure.dpi / 72.0 153 elif self.unit[:4] == "axes": 154 b = TransformedBbox(Bbox.unit(), self.ax.transAxes) 155 dic = { 156 "max": max(b.width, b.height), 157 "min": min(b.width, b.height), 158 "width": b.width, 159 "height": b.height, 160 } 161 factor = dic[self.unit[5:]] 162 return self.size * factor
Draw the arc to the given renderer.
Notes
Ellipses are normally drawn using an approximation that uses eight cubic Bezier splines. The error of this approximation is 1.89818e-6, according to this unverified source:
Lancaster, Don. Approximating a Circle or an Ellipse Using Four Bezier Cubic Splines.
https://www.tinaja.com/glib/ellipse4.pdf
There is a use case where very large ellipses must be drawn with very high accuracy, and it is too expensive to render the entire ellipse with enough segments (either splines or line segments). Therefore, in the case where either radius of the ellipse is large enough that the error of the spline approximation will be visible (greater than one pixel offset from the ideal), a different technique is used.
In that case, only the visible parts of the ellipse are drawn, with each visible arc using a fixed number of spline segments (8). The algorithm proceeds as follows:
The points where the ellipse intersects the axes (or figure) bounding box are located. (This is done by performing an inverse transformation on the bbox such that it is relative to the unit circle -- this makes the intersection calculation much easier than doing rotated ellipse intersection directly.)
This uses the "line intersecting a circle" algorithm from:
Vince, John. *Geometry for Computer Graphics: Formulae, Examples & Proofs.* London: Springer-Verlag, 2005.The angles of each of the intersection points are calculated.
Proceeding counterclockwise starting in the positive x-direction, each of the visible arc-segments between the pairs of vertices are drawn using the Bezier arc approximation technique implemented in
.Path.arc.
200 def update_text(self): 201 c = self._center 202 s = self.get_size() 203 angle_span = (self.theta2 - self.theta1) % 360 204 angle = np.deg2rad(self.theta1 + angle_span / 2) 205 r = s / 2 206 if self.textposition == "inside": 207 r = s / np.interp(angle_span, [60, 90, 135, 180], [3.3, 3.5, 3.8, 4]) 208 self.text.xy = c + r * np.array([np.cos(angle), np.sin(angle)]) 209 if self.textposition == "outside": 210 211 def R90(a, r, w, h): 212 if a < np.arctan(h / 2 / (r + w / 2)): 213 return np.sqrt((r + w / 2) ** 2 + (np.tan(a) * (r + w / 2)) ** 2) 214 else: 215 c = np.sqrt((w / 2) ** 2 + (h / 2) ** 2) 216 T = np.arcsin(c * np.cos(np.pi / 2 - a + np.arcsin(h / 2 / c)) / r) 217 xy = r * np.array([np.cos(a + T), np.sin(a + T)]) 218 xy += np.array([w / 2, h / 2]) 219 return np.sqrt(np.sum(xy**2)) 220 221 def R(a, r, w, h): 222 aa = (a % (np.pi / 4)) * ((a % (np.pi / 2)) <= np.pi / 4) + ( 223 np.pi / 4 - (a % (np.pi / 4)) 224 ) * ((a % (np.pi / 2)) >= np.pi / 4) 225 return R90(aa, r, *[w, h][:: int(np.sign(np.cos(2 * a)))]) 226 227 bbox = self.text.get_window_extent() 228 X = R(angle, r, bbox.width, bbox.height) 229 trans = self.ax.figure.dpi_scale_trans.inverted() 230 offs = trans.transform(((X - s / 2), 0))[0] * 72 # !!! 231 self.text.set_position([offs * np.cos(angle), offs * np.sin(angle)])
148 cls.set = lambda self, **kwargs: Artist.set(self, **kwargs)
Set multiple properties at once.
Supported properties are
Properties:
agg_filter: a filter function, which takes a (m, n, 3) float array and a dpi value, and returns a (m, n, 3) array and two offsets from the bottom left corner of the image
alpha: scalar or None
angle: float
animated: bool
antialiased or aa: bool or None
capstyle: .CapStyle or {'butt', 'projecting', 'round'}
center: unknown
clip_box: ~matplotlib.transforms.BboxBase or None
clip_on: bool
clip_path: Patch or (Path, Transform) or None
color: :mpltype:color
edgecolor or ec: :mpltype:color or None
facecolor or fc: :mpltype:color or None
figure: ~matplotlib.figure.Figure
fill: bool
gid: str
hatch: {'/', '\', '|', '-', '+', 'x', 'o', 'O', '.', '*'}
height: float
in_layout: bool
joinstyle: .JoinStyle or {'miter', 'round', 'bevel'}
label: object
linestyle or ls: {'-', '--', '-.', ':', '', (offset, on-off-seq), ...}
linewidth or lw: float or None
mouseover: bool
path_effects: list of .AbstractPathEffect
picker: None or bool or float or callable
rasterized: bool
size: unknown
sketch_params: (scale: float, length: float, randomness: float)
snap: bool or None
theta: unknown
transform: ~matplotlib.transforms.Transform
url: str
visible: bool
width: float
zorder: float
235def plot_angle(ax, pos, angle, length=0.95, acol="C0", **kwargs): 236 vec2 = np.array([np.cos(np.deg2rad(angle)), np.sin(np.deg2rad(angle))]) 237 xy = np.c_[[length, 0], [0, 0], vec2 * length].T + np.array(pos) 238 ax.plot(*xy.T, color=acol) 239 return AngleAnnotation(pos, xy[0], xy[2], ax=ax, **kwargs)