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`
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, 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.

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

ax
vec1
vec2
size
unit
textposition
kw
text
def get_size(self):
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
def set_size(self, size):
164    def set_size(self, size):
165        self.size = size
def get_center_in_pixels(self):
167    def get_center_in_pixels(self):
168        """return center in pixels"""
169        return self.ax.transData.transform(self._xydata)

return center in pixels

def set_center(self, xy):
171    def set_center(self, xy):
172        """set center in data coordinates"""
173        self._xydata = xy

set center in data coordinates

def get_theta(self, vec):
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]))
def get_theta1(self):
179    def get_theta1(self):
180        return self.get_theta(self.vec1)
def get_theta2(self):
182    def get_theta2(self):
183        return self.get_theta(self.vec2)
def set_theta(self, angle):
185    def set_theta(self, angle):
186        pass
theta1
179    def get_theta1(self):
180        return self.get_theta(self.vec1)
theta2
182    def get_theta2(self):
183        return self.get_theta(self.vec2)
width
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
height
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
def draw(self, renderer):
196    def draw(self, renderer):
197        self.update_text()
198        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):
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)])
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):
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)