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, 132 size, 133 size, 134 angle=0.0, 135 theta1=self.theta1, 136 theta2=self.theta2, 137 **kwargs 138 ) 139 140 self.set_transform(IdentityTransform()) 141 self.ax.add_patch(self) 142 143 self.kw = dict( 144 ha="center", 145 va="center", 146 xycoords=IdentityTransform(), 147 xytext=(0, 0), 148 textcoords="offset points", 149 annotation_clip=True, 150 ) 151 self.kw.update(text_kw or {}) 152 self.text = ax.annotate(text, xy=self._center, **self.kw) 153 154 def get_size(self): 155 factor = 1.0 156 if self.unit == "points": 157 factor = self.ax.figure.dpi / 72.0 158 elif self.unit[:4] == "axes": 159 b = TransformedBbox(Bbox.unit(), self.ax.transAxes) 160 dic = { 161 "max": max(b.width, b.height), 162 "min": min(b.width, b.height), 163 "width": b.width, 164 "height": b.height, 165 } 166 factor = dic[self.unit[5:]] 167 return self.size * factor 168 169 def set_size(self, size): 170 self.size = size 171 172 def get_center_in_pixels(self): 173 """return center in pixels""" 174 return self.ax.transData.transform(self._xydata) 175 176 def set_center(self, xy): 177 """set center in data coordinates""" 178 self._xydata = xy 179 180 def get_theta(self, vec): 181 vec_in_pixels = self.ax.transData.transform(vec) - self._center 182 return np.rad2deg(np.arctan2(vec_in_pixels[1], vec_in_pixels[0])) 183 184 def get_theta1(self): 185 return self.get_theta(self.vec1) 186 187 def get_theta2(self): 188 return self.get_theta(self.vec2) 189 190 def set_theta(self, angle): 191 pass 192 193 # Redefine attributes of the Arc to always give values in pixel space 194 _center = property(get_center_in_pixels, set_center) 195 theta1 = property(get_theta1, set_theta) 196 theta2 = property(get_theta2, set_theta) 197 width = property(get_size, set_size) 198 height = property(get_size, set_size) 199 200 # The following two methods are needed to update the text position. 201 def draw(self, renderer): 202 self.update_text() 203 super().draw(renderer) 204 205 def update_text(self): 206 c = self._center 207 s = self.get_size() 208 angle_span = (self.theta2 - self.theta1) % 360 209 angle = np.deg2rad(self.theta1 + angle_span / 2) 210 r = s / 2 211 if self.textposition == "inside": 212 r = s / np.interp(angle_span, [60, 90, 135, 180], [3.3, 3.5, 3.8, 4]) 213 self.text.xy = c + r * np.array([np.cos(angle), np.sin(angle)]) 214 if self.textposition == "outside": 215 216 def R90(a, r, w, h): 217 if a < np.arctan(h / 2 / (r + w / 2)): 218 return np.sqrt((r + w / 2) ** 2 + (np.tan(a) * (r + w / 2)) ** 2) 219 else: 220 c = np.sqrt((w / 2) ** 2 + (h / 2) ** 2) 221 T = np.arcsin(c * np.cos(np.pi / 2 - a + np.arcsin(h / 2 / c)) / r) 222 xy = r * np.array([np.cos(a + T), np.sin(a + T)]) 223 xy += np.array([w / 2, h / 2]) 224 return np.sqrt(np.sum(xy**2)) 225 226 def R(a, r, w, h): 227 aa = (a % (np.pi / 4)) * ((a % (np.pi / 2)) <= np.pi / 4) + ( 228 np.pi / 4 - (a % (np.pi / 4)) 229 ) * ((a % (np.pi / 2)) >= np.pi / 4) 230 return R90(aa, r, *[w, h][:: int(np.sign(np.cos(2 * a)))]) 231 232 bbox = self.text.get_window_extent() 233 X = R(angle, r, bbox.width, bbox.height) 234 trans = self.ax.figure.dpi_scale_trans.inverted() 235 offs = trans.transform(((X - s / 2), 0))[0] * 72 # !!! 236 self.text.set_position([offs * np.cos(angle), offs * np.sin(angle)]) 237 238 239# Helper function to draw angle easily. 240def plot_angle(ax, pos, angle, length=0.95, acol="C0", **kwargs): 241 vec2 = np.array([np.cos(np.deg2rad(angle)), np.sin(np.deg2rad(angle))]) 242 xy = np.c_[[length, 0], [0, 0], vec2 * length].T + np.array(pos) 243 ax.plot(*xy.T, color=acol) 244 return AngleAnnotation(pos, xy[0], xy[2], ax=ax, **kwargs) 245 246 247######################################################################### 248# ``AngleLabel`` options 249# ~~~~~~~~~~~~~~~~~~~~~~ 250# 251# The *textposition* and *unit* keyword arguments may be used to modify the 252# location of the text label, as shown below: 253 254 255""" 256fig, (ax1, ax2) = plt.subplots(nrows=2, sharex=True) 257fig.suptitle("AngleLabel keyword arguments") 258fig.canvas.draw() # Need to draw the figure to define renderer 259 260# Showcase different text positions. 261ax1.margins(y=0.4) 262ax1.set_title("textposition") 263kw = dict(size=75, unit="points", text=r"$60°$") 264 265am6 = plot_angle(ax1, (2.0, 0), 60, textposition="inside", **kw) 266am7 = plot_angle(ax1, (3.5, 0), 60, textposition="outside", **kw) 267am8 = plot_angle(ax1, (5.0, 0), 60, textposition="edge", 268 text_kw=dict(bbox=dict(boxstyle="round", fc="w")), **kw) 269am9 = plot_angle(ax1, (6.5, 0), 60, textposition="edge", 270 text_kw=dict(xytext=(30, 20), arrowprops=dict(arrowstyle="->", 271 connectionstyle="arc3,rad=-0.2")), **kw) 272 273for x, text in zip([2.0, 3.5, 5.0, 6.5], ['"inside"', '"outside"', '"edge"', 274 '"edge", custom arrow']): 275 ax1.annotate(text, xy=(x, 0), xycoords=ax1.get_xaxis_transform(), 276 bbox=dict(boxstyle="round", fc="w"), ha="left", fontsize=8, 277 annotation_clip=True) 278 279# Showcase different size units. The effect of this can best be observed 280# by interactively changing the figure size 281ax2.margins(y=0.4) 282ax2.set_title("unit") 283kw = dict(text=r"$60°$", textposition="outside") 284 285am10 = plot_angle(ax2, (2.0, 0), 60, size=50, unit="pixels", **kw) 286am11 = plot_angle(ax2, (3.5, 0), 60, size=50, unit="points", **kw) 287am12 = plot_angle(ax2, (5.0, 0), 60, size=0.25, unit="axes min", **kw) 288am13 = plot_angle(ax2, (6.5, 0), 60, size=0.25, unit="axes max", **kw) 289 290for x, text in zip([2.0, 3.5, 5.0, 6.5], ['"pixels"', '"points"', 291 '"axes min"', '"axes max"']): 292 ax2.annotate(text, xy=(x, 0), xycoords=ax2.get_xaxis_transform(), 293 bbox=dict(boxstyle="round", fc="w"), ha="left", fontsize=8, 294 annotation_clip=True) 295 296plt.show() 297 298""" 299 300 301############################################################################# 302# 303# .. admonition:: References 304# 305# The use of the following functions, methods, classes and modules is shown 306# in this example: 307# 308# - `matplotlib.patches.Arc` 309# - `matplotlib.axes.Axes.annotate` / `matplotlib.pyplot.annotate` 310# - `matplotlib.text.Annotation` 311# - `matplotlib.transforms.IdentityTransform` 312# - `matplotlib.transforms.TransformedBbox` 313# - `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, 133 size, 134 size, 135 angle=0.0, 136 theta1=self.theta1, 137 theta2=self.theta2, 138 **kwargs 139 ) 140 141 self.set_transform(IdentityTransform()) 142 self.ax.add_patch(self) 143 144 self.kw = dict( 145 ha="center", 146 va="center", 147 xycoords=IdentityTransform(), 148 xytext=(0, 0), 149 textcoords="offset points", 150 annotation_clip=True, 151 ) 152 self.kw.update(text_kw or {}) 153 self.text = ax.annotate(text, xy=self._center, **self.kw) 154 155 def get_size(self): 156 factor = 1.0 157 if self.unit == "points": 158 factor = self.ax.figure.dpi / 72.0 159 elif self.unit[:4] == "axes": 160 b = TransformedBbox(Bbox.unit(), self.ax.transAxes) 161 dic = { 162 "max": max(b.width, b.height), 163 "min": min(b.width, b.height), 164 "width": b.width, 165 "height": b.height, 166 } 167 factor = dic[self.unit[5:]] 168 return self.size * factor 169 170 def set_size(self, size): 171 self.size = size 172 173 def get_center_in_pixels(self): 174 """return center in pixels""" 175 return self.ax.transData.transform(self._xydata) 176 177 def set_center(self, xy): 178 """set center in data coordinates""" 179 self._xydata = xy 180 181 def get_theta(self, vec): 182 vec_in_pixels = self.ax.transData.transform(vec) - self._center 183 return np.rad2deg(np.arctan2(vec_in_pixels[1], vec_in_pixels[0])) 184 185 def get_theta1(self): 186 return self.get_theta(self.vec1) 187 188 def get_theta2(self): 189 return self.get_theta(self.vec2) 190 191 def set_theta(self, angle): 192 pass 193 194 # Redefine attributes of the Arc to always give values in pixel space 195 _center = property(get_center_in_pixels, set_center) 196 theta1 = property(get_theta1, set_theta) 197 theta2 = property(get_theta2, set_theta) 198 width = property(get_size, set_size) 199 height = property(get_size, set_size) 200 201 # The following two methods are needed to update the text position. 202 def draw(self, renderer): 203 self.update_text() 204 super().draw(renderer) 205 206 def update_text(self): 207 c = self._center 208 s = self.get_size() 209 angle_span = (self.theta2 - self.theta1) % 360 210 angle = np.deg2rad(self.theta1 + angle_span / 2) 211 r = s / 2 212 if self.textposition == "inside": 213 r = s / np.interp(angle_span, [60, 90, 135, 180], [3.3, 3.5, 3.8, 4]) 214 self.text.xy = c + r * np.array([np.cos(angle), np.sin(angle)]) 215 if self.textposition == "outside": 216 217 def R90(a, r, w, h): 218 if a < np.arctan(h / 2 / (r + w / 2)): 219 return np.sqrt((r + w / 2) ** 2 + (np.tan(a) * (r + w / 2)) ** 2) 220 else: 221 c = np.sqrt((w / 2) ** 2 + (h / 2) ** 2) 222 T = np.arcsin(c * np.cos(np.pi / 2 - a + np.arcsin(h / 2 / c)) / r) 223 xy = r * np.array([np.cos(a + T), np.sin(a + T)]) 224 xy += np.array([w / 2, h / 2]) 225 return np.sqrt(np.sum(xy**2)) 226 227 def R(a, r, w, h): 228 aa = (a % (np.pi / 4)) * ((a % (np.pi / 2)) <= np.pi / 4) + ( 229 np.pi / 4 - (a % (np.pi / 4)) 230 ) * ((a % (np.pi / 2)) >= np.pi / 4) 231 return R90(aa, r, *[w, h][:: int(np.sign(np.cos(2 * a)))]) 232 233 bbox = self.text.get_window_extent() 234 X = R(angle, r, bbox.width, bbox.height) 235 trans = self.ax.figure.dpi_scale_trans.inverted() 236 offs = trans.transform(((X - s / 2), 0))[0] * 72 # !!! 237 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, 133 size, 134 size, 135 angle=0.0, 136 theta1=self.theta1, 137 theta2=self.theta2, 138 **kwargs 139 ) 140 141 self.set_transform(IdentityTransform()) 142 self.ax.add_patch(self) 143 144 self.kw = dict( 145 ha="center", 146 va="center", 147 xycoords=IdentityTransform(), 148 xytext=(0, 0), 149 textcoords="offset points", 150 annotation_clip=True, 151 ) 152 self.kw.update(text_kw or {}) 153 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.
155 def get_size(self): 156 factor = 1.0 157 if self.unit == "points": 158 factor = self.ax.figure.dpi / 72.0 159 elif self.unit[:4] == "axes": 160 b = TransformedBbox(Bbox.unit(), self.ax.transAxes) 161 dic = { 162 "max": max(b.width, b.height), 163 "min": min(b.width, b.height), 164 "width": b.width, 165 "height": b.height, 166 } 167 factor = dic[self.unit[5:]] 168 return self.size * factor
173 def get_center_in_pixels(self): 174 """return center in pixels""" 175 return self.ax.transData.transform(self._xydata)
return center in pixels
155 def get_size(self): 156 factor = 1.0 157 if self.unit == "points": 158 factor = self.ax.figure.dpi / 72.0 159 elif self.unit[:4] == "axes": 160 b = TransformedBbox(Bbox.unit(), self.ax.transAxes) 161 dic = { 162 "max": max(b.width, b.height), 163 "min": min(b.width, b.height), 164 "width": b.width, 165 "height": b.height, 166 } 167 factor = dic[self.unit[5:]] 168 return self.size * factor
155 def get_size(self): 156 factor = 1.0 157 if self.unit == "points": 158 factor = self.ax.figure.dpi / 72.0 159 elif self.unit[:4] == "axes": 160 b = TransformedBbox(Bbox.unit(), self.ax.transAxes) 161 dic = { 162 "max": max(b.width, b.height), 163 "min": min(b.width, b.height), 164 "width": b.width, 165 "height": b.height, 166 } 167 factor = dic[self.unit[5:]] 168 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
.
206 def update_text(self): 207 c = self._center 208 s = self.get_size() 209 angle_span = (self.theta2 - self.theta1) % 360 210 angle = np.deg2rad(self.theta1 + angle_span / 2) 211 r = s / 2 212 if self.textposition == "inside": 213 r = s / np.interp(angle_span, [60, 90, 135, 180], [3.3, 3.5, 3.8, 4]) 214 self.text.xy = c + r * np.array([np.cos(angle), np.sin(angle)]) 215 if self.textposition == "outside": 216 217 def R90(a, r, w, h): 218 if a < np.arctan(h / 2 / (r + w / 2)): 219 return np.sqrt((r + w / 2) ** 2 + (np.tan(a) * (r + w / 2)) ** 2) 220 else: 221 c = np.sqrt((w / 2) ** 2 + (h / 2) ** 2) 222 T = np.arcsin(c * np.cos(np.pi / 2 - a + np.arcsin(h / 2 / c)) / r) 223 xy = r * np.array([np.cos(a + T), np.sin(a + T)]) 224 xy += np.array([w / 2, h / 2]) 225 return np.sqrt(np.sum(xy**2)) 226 227 def R(a, r, w, h): 228 aa = (a % (np.pi / 4)) * ((a % (np.pi / 2)) <= np.pi / 4) + ( 229 np.pi / 4 - (a % (np.pi / 4)) 230 ) * ((a % (np.pi / 2)) >= np.pi / 4) 231 return R90(aa, r, *[w, h][:: int(np.sign(np.cos(2 * a)))]) 232 233 bbox = self.text.get_window_extent() 234 X = R(angle, r, bbox.width, bbox.height) 235 trans = self.ax.figure.dpi_scale_trans.inverted() 236 offs = trans.transform(((X - s / 2), 0))[0] * 72 # !!! 237 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
241def plot_angle(ax, pos, angle, length=0.95, acol="C0", **kwargs): 242 vec2 = np.array([np.cos(np.deg2rad(angle)), np.sin(np.deg2rad(angle))]) 243 xy = np.c_[[length, 0], [0, 0], vec2 * length].T + np.array(pos) 244 ax.plot(*xy.T, color=acol) 245 return AngleAnnotation(pos, xy[0], xy[2], ax=ax, **kwargs)