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`
class AngleAnnotation(matplotlib.patches.Arc):
 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.

AngleAnnotation( xy, p1, p2, size=75, unit='points', ax=None, text='', textposition='inside', text_kw=None, **kwargs)
 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.

ax
vec1
vec2
size
unit
textposition
kw
text
def get_size(self):
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
def set_size(self, size):
170    def set_size(self, size):
171        self.size = size
def get_center_in_pixels(self):
173    def get_center_in_pixels(self):
174        """return center in pixels"""
175        return self.ax.transData.transform(self._xydata)

return center in pixels

def set_center(self, xy):
177    def set_center(self, xy):
178        """set center in data coordinates"""
179        self._xydata = xy

set center in data coordinates

def get_theta(self, vec):
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]))
def get_theta1(self):
185    def get_theta1(self):
186        return self.get_theta(self.vec1)
def get_theta2(self):
188    def get_theta2(self):
189        return self.get_theta(self.vec2)
def set_theta(self, angle):
191    def set_theta(self, angle):
192        pass
theta1
185    def get_theta1(self):
186        return self.get_theta(self.vec1)
theta2
188    def get_theta2(self):
189        return self.get_theta(self.vec2)
width
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
height
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
def draw(self, renderer):
202    def draw(self, renderer):
203        self.update_text()
204        super().draw(renderer)

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:

  1. 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.
    
  2. The angles of each of the intersection points are calculated.

  3. 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.

def update_text(self):
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)])
def set( self, *, agg_filter=<UNSET>, alpha=<UNSET>, angle=<UNSET>, animated=<UNSET>, antialiased=<UNSET>, capstyle=<UNSET>, center=<UNSET>, clip_box=<UNSET>, clip_on=<UNSET>, clip_path=<UNSET>, color=<UNSET>, edgecolor=<UNSET>, facecolor=<UNSET>, fill=<UNSET>, gid=<UNSET>, hatch=<UNSET>, height=<UNSET>, in_layout=<UNSET>, joinstyle=<UNSET>, label=<UNSET>, linestyle=<UNSET>, linewidth=<UNSET>, mouseover=<UNSET>, path_effects=<UNSET>, picker=<UNSET>, rasterized=<UNSET>, size=<UNSET>, sketch_params=<UNSET>, snap=<UNSET>, theta=<UNSET>, transform=<UNSET>, url=<UNSET>, visible=<UNSET>, width=<UNSET>, zorder=<UNSET>):
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

def plot_angle(ax, pos, angle, length=0.95, acol='C0', **kwargs):
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)