From 518b662b8d84891e528c44e61f64046a3df83527 Mon Sep 17 00:00:00 2001 From: Daniel Ingram Date: Sat, 13 Oct 2018 10:31:49 -0400 Subject: [PATCH 01/14] Example showing scale-invariant angle arc --- examples/lines_bars_and_markers/angle_arc.py | 92 ++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 examples/lines_bars_and_markers/angle_arc.py diff --git a/examples/lines_bars_and_markers/angle_arc.py b/examples/lines_bars_and_markers/angle_arc.py new file mode 100644 index 000000000000..79e10c649aab --- /dev/null +++ b/examples/lines_bars_and_markers/angle_arc.py @@ -0,0 +1,92 @@ +""" +============== +Angle Arc Demo +============== + +Draw an angle arc between two vectors +""" +from math import atan2, sqrt, pi +import matplotlib.pyplot as plt +import matplotlib.patches as mpatches + +fig, ax = plt.subplots() + +vec1 = ax.arrow(0, 0, 2, 1, head_width=0.05) +vec2 = ax.arrow(0, 0, -1, 2, head_width=0.05) + + +def get_vector_angle(vec): + """ + Finds the angle of a FancyArrow. Probably + a better way to do this. + + Parameters + ---------- + vec : matplotlib.patches.FancyArrow + + Returns + ------- + float + Angle in radians between +x axis + and vec. + """ + xy = vec.get_xy() + dx = max(xy[:, 0], key=abs) - min(xy[:, 0], key=abs) + dy = max(xy[:, 1], key=abs) - min(xy[:, 1], key=abs) + return atan2(dy, dx)*180/pi + + +def draw_arc_between_vectors(ax, vec1, vec2): + """ + Draws a scale-invariant arc between two vectors. + + Arc will be drawn counterclockwise if the angle + of vec1 is smaller than the angle of vec2 and + will be drawn clockwise otherwise. Arc will be + drawn as a mpatches.Arc on the provided axes. + + Parameters + ---------- + ax : matplotlib.axes._subplots.AxesSubplot + The axes on which vec1 and vec2 are drawn + vec1 : matplotlib.patches.FancyArrow + Vector 1 + vec2 : matplotlib.patches.FancyArrow + Vector 2 + """ + x0, y0 = ax.transData.transform((0, 0)) + x1, y1 = ax.transData.transform((1, 1)) + dx = x1 - x0 + dy = y1 - y0 + d = sqrt(dx**2 + dy**2) + width = d/dx + height = d/dy + norm = sqrt(width**2 + height**2) + width /= norm + height /= norm + theta1 = get_vector_angle(vec1) + theta2 = get_vector_angle(vec2) + arc = mpatches.Arc((0, 0), width, height, theta1=theta1, theta2=theta2) + try: + ax.patches[0].remove() + except: + pass + ax.add_patch(arc) + print(ax.patches[0].gid) + + +def fig_resize(event): + draw_arc_between_vectors(ax, vec1, vec2) + + +def axes_resize(ax): + draw_arc_between_vectors(ax, vec1, vec2) + +fig.canvas.mpl_connect("resize_event", fig_resize) +ax.callbacks.connect("xlim_changed", axes_resize) +ax.callbacks.connect("ylim_changed", axes_resize) + +plt.xlim(-3, 3) +plt.ylim(-3, 3) + +plt.show() From b6134dc0325f860e3f462929fe9f37151b2d557a Mon Sep 17 00:00:00 2001 From: Daniel Ingram Date: Sat, 13 Oct 2018 11:48:52 -0400 Subject: [PATCH 02/14] Remove arc based on gid --- examples/lines_bars_and_markers/angle_arc.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/examples/lines_bars_and_markers/angle_arc.py b/examples/lines_bars_and_markers/angle_arc.py index 79e10c649aab..7e238662456d 100644 --- a/examples/lines_bars_and_markers/angle_arc.py +++ b/examples/lines_bars_and_markers/angle_arc.py @@ -66,13 +66,10 @@ def draw_arc_between_vectors(ax, vec1, vec2): height /= norm theta1 = get_vector_angle(vec1) theta2 = get_vector_angle(vec2) - arc = mpatches.Arc((0, 0), width, height, theta1=theta1, theta2=theta2) - try: - ax.patches[0].remove() - except: - pass + arc = mpatches.Arc( + (0, 0), width, height, theta1=theta1, theta2=theta2, gid="angle_arc") + [p.remove() for p in ax.patches if p.get_gid() == "angle_arc"] ax.add_patch(arc) - print(ax.patches[0].gid) def fig_resize(event): From 5725c83798bed196c8c26e1cd0d36a0027de2028 Mon Sep 17 00:00:00 2001 From: Daniel Ingram Date: Sat, 13 Oct 2018 13:21:48 -0400 Subject: [PATCH 03/14] Made radius scale invariant --- examples/lines_bars_and_markers/angle_arc.py | 29 ++++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/examples/lines_bars_and_markers/angle_arc.py b/examples/lines_bars_and_markers/angle_arc.py index 7e238662456d..c7bd9a15fa09 100644 --- a/examples/lines_bars_and_markers/angle_arc.py +++ b/examples/lines_bars_and_markers/angle_arc.py @@ -15,13 +15,15 @@ vec2 = ax.arrow(0, 0, -1, 2, head_width=0.05) -def get_vector_angle(vec): +def get_vector_angle(trans, vec): """ Finds the angle of a FancyArrow. Probably a better way to do this. Parameters ---------- + trans : matplotlib.transforms.CompositeGenericTransform + Transformation from data coords to axes coords vec : matplotlib.patches.FancyArrow Returns @@ -30,7 +32,8 @@ def get_vector_angle(vec): Angle in radians between +x axis and vec. """ - xy = vec.get_xy() + #Shift axes coords to -0.5, 0.5 for proper angle calculation + xy = trans.transform(vec.get_xy()) - 0.5 dx = max(xy[:, 0], key=abs) - min(xy[:, 0], key=abs) dy = max(xy[:, 1], key=abs) - min(xy[:, 1], key=abs) return atan2(dy, dx)*180/pi @@ -54,20 +57,22 @@ def draw_arc_between_vectors(ax, vec1, vec2): vec2 : matplotlib.patches.FancyArrow Vector 2 """ - x0, y0 = ax.transData.transform((0, 0)) - x1, y1 = ax.transData.transform((1, 1)) + x0, y0 = ax.transAxes.transform((0, 0)) + x1, y1 = ax.transAxes.transform((1, 1)) dx = x1 - x0 dy = y1 - y0 d = sqrt(dx**2 + dy**2) - width = d/dx - height = d/dy - norm = sqrt(width**2 + height**2) - width /= norm - height /= norm - theta1 = get_vector_angle(vec1) - theta2 = get_vector_angle(vec2) + width = 0.1*d/dx + height = 0.1*d/dy + trans = ax.transData + ax.transAxes.inverted() + theta1 = get_vector_angle(trans, vec1) + theta2 = get_vector_angle(trans, vec2) arc = mpatches.Arc( - (0, 0), width, height, theta1=theta1, theta2=theta2, gid="angle_arc") + trans.transform((0, 0)), + width, height, + theta1=theta1, theta2=theta2, + gid="angle_arc", + transform=ax.transAxes) [p.remove() for p in ax.patches if p.get_gid() == "angle_arc"] ax.add_patch(arc) From 34d3a2d0314227469da7e274045ada1cf7a23a4d Mon Sep 17 00:00:00 2001 From: Daniel Ingram Date: Mon, 15 Oct 2018 20:14:36 -0400 Subject: [PATCH 04/14] Add option for relative units for arc radius --- examples/lines_bars_and_markers/AngleArc.py | 68 ++++++++++++++ examples/lines_bars_and_markers/angle_arc.py | 94 -------------------- 2 files changed, 68 insertions(+), 94 deletions(-) create mode 100644 examples/lines_bars_and_markers/AngleArc.py delete mode 100644 examples/lines_bars_and_markers/angle_arc.py diff --git a/examples/lines_bars_and_markers/AngleArc.py b/examples/lines_bars_and_markers/AngleArc.py new file mode 100644 index 000000000000..8fbd4819184f --- /dev/null +++ b/examples/lines_bars_and_markers/AngleArc.py @@ -0,0 +1,68 @@ +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.patches import Arc +from matplotlib.transforms import IdentityTransform + + +class AngleArc(Arc): + def __init__(self, xy, vec1, vec2, size=100, units="pixels", + ax=None, fig=None, **kwargs): + self._xydata = xy # in data coordinates + self.ax = ax or plt.gca() + self.fig = fig or plt.gcf() + self.vec1 = vec1 # tuple or array of coordinates, relative to xy + self.vec2 = vec2 # tuple or array of coordinates, relative to xy + self.size = size + + super().__init__(self._xydata, size, size, angle=0.0, + theta1=self.theta1, theta2=self.theta2, **kwargs) + + self.set_transform(IdentityTransform()) + + if units == "relative": + fig.canvas.mpl_connect("resize_event", self._resize) + + self.ax.add_patch(self) + + def _resize(self, event): + x0, y0 = self.ax.transAxes.transform((0, 0)) + x1, y1 = self.ax.transAxes.transform((1, 1)) + dx = x1 - x0 + dy = y1 - y0 + smallest = min(dx, dy) + self.width = 0.25*dx + self.height = 0.25*dx + + def get_center_pixels(self): + """ return center in pixel coordinates """ + return self.ax.transData.transform(self._xydata) + + def set_center(self, xy): + """ set center in data coordinates """ + self._xydata = xy + + _center = property(get_center_pixels, set_center) + + def get_theta(self, vec): + vec_in_pixels = self.ax.transData.transform(vec) - self._center + return np.rad2deg(np.arctan2(vec_in_pixels[1], vec_in_pixels[0])) + + def get_theta1(self): + return self.get_theta(self.vec1) + + def get_theta2(self): + return self.get_theta(self.vec2) + + def set_theta(self, angle): + pass + + theta1 = property(get_theta1, set_theta) + theta2 = property(get_theta2, set_theta) + + +fig, ax = plt.subplots() + +ax.plot([2, .5, 1], [0, .2, 1]) +am = AngleArc((0.5, 0.2), (2, 0), (1, 1), size=100, units="relative", + ax=ax, fig=fig) +plt.show() diff --git a/examples/lines_bars_and_markers/angle_arc.py b/examples/lines_bars_and_markers/angle_arc.py deleted file mode 100644 index c7bd9a15fa09..000000000000 --- a/examples/lines_bars_and_markers/angle_arc.py +++ /dev/null @@ -1,94 +0,0 @@ -""" -============== -Angle Arc Demo -============== - -Draw an angle arc between two vectors -""" -from math import atan2, sqrt, pi -import matplotlib.pyplot as plt -import matplotlib.patches as mpatches - -fig, ax = plt.subplots() - -vec1 = ax.arrow(0, 0, 2, 1, head_width=0.05) -vec2 = ax.arrow(0, 0, -1, 2, head_width=0.05) - - -def get_vector_angle(trans, vec): - """ - Finds the angle of a FancyArrow. Probably - a better way to do this. - - Parameters - ---------- - trans : matplotlib.transforms.CompositeGenericTransform - Transformation from data coords to axes coords - vec : matplotlib.patches.FancyArrow - - Returns - ------- - float - Angle in radians between +x axis - and vec. - """ - #Shift axes coords to -0.5, 0.5 for proper angle calculation - xy = trans.transform(vec.get_xy()) - 0.5 - dx = max(xy[:, 0], key=abs) - min(xy[:, 0], key=abs) - dy = max(xy[:, 1], key=abs) - min(xy[:, 1], key=abs) - return atan2(dy, dx)*180/pi - - -def draw_arc_between_vectors(ax, vec1, vec2): - """ - Draws a scale-invariant arc between two vectors. - - Arc will be drawn counterclockwise if the angle - of vec1 is smaller than the angle of vec2 and - will be drawn clockwise otherwise. Arc will be - drawn as a mpatches.Arc on the provided axes. - - Parameters - ---------- - ax : matplotlib.axes._subplots.AxesSubplot - The axes on which vec1 and vec2 are drawn - vec1 : matplotlib.patches.FancyArrow - Vector 1 - vec2 : matplotlib.patches.FancyArrow - Vector 2 - """ - x0, y0 = ax.transAxes.transform((0, 0)) - x1, y1 = ax.transAxes.transform((1, 1)) - dx = x1 - x0 - dy = y1 - y0 - d = sqrt(dx**2 + dy**2) - width = 0.1*d/dx - height = 0.1*d/dy - trans = ax.transData + ax.transAxes.inverted() - theta1 = get_vector_angle(trans, vec1) - theta2 = get_vector_angle(trans, vec2) - arc = mpatches.Arc( - trans.transform((0, 0)), - width, height, - theta1=theta1, theta2=theta2, - gid="angle_arc", - transform=ax.transAxes) - [p.remove() for p in ax.patches if p.get_gid() == "angle_arc"] - ax.add_patch(arc) - - -def fig_resize(event): - draw_arc_between_vectors(ax, vec1, vec2) - - -def axes_resize(ax): - draw_arc_between_vectors(ax, vec1, vec2) - -fig.canvas.mpl_connect("resize_event", fig_resize) -ax.callbacks.connect("xlim_changed", axes_resize) -ax.callbacks.connect("ylim_changed", axes_resize) - -plt.xlim(-3, 3) -plt.ylim(-3, 3) - -plt.show() From d34b8d9d53b8769bfdbc7aed6297ec91b1649755 Mon Sep 17 00:00:00 2001 From: Daniel Ingram Date: Thu, 18 Oct 2018 11:38:19 -0400 Subject: [PATCH 05/14] Add option for text --- .../lines_bars_and_markers/AngleMarker.py | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 examples/lines_bars_and_markers/AngleMarker.py diff --git a/examples/lines_bars_and_markers/AngleMarker.py b/examples/lines_bars_and_markers/AngleMarker.py new file mode 100644 index 000000000000..997882611586 --- /dev/null +++ b/examples/lines_bars_and_markers/AngleMarker.py @@ -0,0 +1,172 @@ +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.patches import Arc +from matplotlib.text import Text +from matplotlib.transforms import IdentityTransform, TransformedBbox, Bbox + + +class AngleMarker(Arc, Text): + """ + Draws an arc between two vectors which appears circular in display space. + """ + def __init__(self, xy, vec1, vec2, size=100, units="pixels", ax=None, + text='', **kwargs): + """ + Params + ------ + xy, vec1, vec2 : tuple or array of two floats + center position and two points. Angle marker is drawn between the + two vectors connecting vec1 and vec2 with xy, respectively. Units + are data coordinates. + + size : float + diameter of the angle marker in units specified by ``units``. + + units : string + One of the following strings to specify the units of ``size``: + * "pixels" : pixels + * "points" : points, use points instead of pixels to not have a + dependence of 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 marker to + + kwargs : + Further parameters are passed to `matplotlib.patches.Arc`. Use this + to specify, color, linewidth etc of the arc. + + """ + self._xydata = xy # in data coordinates + self.ax = ax or plt.gca() + self.vec1 = vec1 # tuple or array of absolute coordinates + self.vec2 = vec2 # tuple or array of absolute coordinates + self.size = size + self.units = units + + Arc.__init__(self, self._xydata, size, size, angle=0.0, + theta1=self.theta1, theta2=self.theta2, **kwargs) + Text.__init__(self, x=self._x, y=self._y, text=text, **kwargs) + + self.set_transform(IdentityTransform()) + self.ax.add_artist(self) + + def get_size(self): + factor = 1. + if self.units == "points": + factor = self.ax.figure.dpi / 72. + elif self.units[:4] == "axes": + b = TransformedBbox(Bbox.from_bounds(0, 0, 1, 1), + self.ax.transAxes) + dic = {"max": max(b.width, b.height), + "min": min(b.width, b.height), + "width": b.width, "height": b.height} + factor = dic[self.units[5:]] + return self.size * factor + + def set_size(self, size): + self.size = size + + def get_center_in_pixels(self): + """ return center in pixels """ + return self.ax.transData.transform(self._xydata) + + def set_center(self, xy): + """ set center in data coordinates """ + self._xydata = xy + + def get_theta(self, vec): + vec_in_pixels = self.ax.transData.transform(vec) - self._center + return np.rad2deg(np.arctan2(vec_in_pixels[1], vec_in_pixels[0])) + + def get_theta1(self): + return self.get_theta(self.vec1) + + def get_theta2(self): + return self.get_theta(self.vec2) + + def set_theta(self, angle): + pass + + def get_x_text(self): + return self._xydata[0] + 3*self.size + + def get_y_text(self): + return self._xydata[1] + 3*self.size + + def set_xy_text(self, xy): + pass + + def set_color(self, color): + Arc.set_color(self, color) + Text.set_color(self, color) + + def draw(self, renderer): + Arc.draw(self, renderer) + Text.draw(self, renderer) + + _center = property(get_center_in_pixels, set_center) + theta1 = property(get_theta1, set_theta) + theta2 = property(get_theta2, set_theta) + width = property(get_size, set_size) + height = property(get_size, set_size) + _x = property(get_x_text, set_xy_text) + _y = property(get_y_text, set_xy_text) + + +fig, ax = plt.subplots() + +ax.plot([2, .5, 1], [0, .2, 1]) +am = AngleMarker((.5, .2), (2, 0), (1, 1), size=0.25, units="axes max", ax=ax, + text=r"$\theta$") +plt.show() + +''' +def testing(size=0.25, units="axes fraction", dpi=100, fs=(6.4, 5), + show=False): + + fig, axes = plt.subplots(2, 2, sharex="col", sharey="row", dpi=dpi, + figsize=fs, + gridspec_kw=dict(width_ratios=[1, 3], + height_ratios=[3, 1])) + + def plot_angle(ax, pos, vec1, vec2, acol="C0", **kwargs): + ax.plot([vec1[0], pos[0], vec2[0]], [vec1[1], pos[1], vec2[1]], + color=acol) + am = AngleMarker(pos, vec1, vec2, ax=ax, text=r"$\theta$", **kwargs) + + tx = "figsize={}, dpi={}, arcsize={} {}".format(fs, dpi, size, units) + axes[0, 1].set_title(tx, loc="right", size=9) + kw = dict(size=size, units=units) + p = (.5, .2), (2, 0), (1, 1) + plot_angle(axes[0, 0], *p, **kw) + plot_angle(axes[0, 1], *p, **kw) + plot_angle(axes[1, 1], *p, **kw) + kw.update(acol="limegreen") + plot_angle(axes[0, 0], (1.2, 0), (1, -1), (1.3, -.8), **kw) + plot_angle(axes[1, 1], (0.2, 1), (0, 0), (.3, .2), **kw) + plot_angle(axes[0, 1], (0.2, 0), (0, -1), (.3, -.8), **kw) + kw.update(acol="crimson") + plot_angle(axes[1, 0], (1, .5), (1, 1), (2, .5), **kw) + + fig.tight_layout() + fig.savefig(tx.replace("=", "_") + ".png") + fig.savefig(tx.replace("=", "_") + ".pdf") + if show: + plt.show() + + +s = [(0.25, "axes min"), (0.25, "axes max"), + (0.25, "axes width"), (0.25, "axes height"), + (100, "pixels"), (72, "points")] +d = [72, 144] +f = [(6.4, 5), (12.8, 10)] + +import itertools + +for (size, unit), dpi, fs in itertools.product(s, d, f): + testing(size=size, units=unit, dpi=dpi, fs=fs) +''' From 27bd23be06be9506421e7d8fe3852fd046f377d7 Mon Sep 17 00:00:00 2001 From: Daniel Ingram Date: Sat, 24 Nov 2018 15:40:53 -0500 Subject: [PATCH 06/14] Add annotation to AngleMarker --- .../lines_bars_and_markers/AngleMarker.py | 56 ++++++++++--------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/examples/lines_bars_and_markers/AngleMarker.py b/examples/lines_bars_and_markers/AngleMarker.py index 997882611586..f67c864f7910 100644 --- a/examples/lines_bars_and_markers/AngleMarker.py +++ b/examples/lines_bars_and_markers/AngleMarker.py @@ -1,11 +1,11 @@ import numpy as np import matplotlib.pyplot as plt from matplotlib.patches import Arc -from matplotlib.text import Text +from matplotlib.text import Annotation from matplotlib.transforms import IdentityTransform, TransformedBbox, Bbox -class AngleMarker(Arc, Text): +class AngleMarker(Arc): """ Draws an arc between two vectors which appears circular in display space. """ @@ -47,12 +47,28 @@ def __init__(self, xy, vec1, vec2, size=100, units="pixels", ax=None, self.size = size self.units = units + if self.theta1 > self.theta2: + self.vec1, self.vec2 = self.vec2, self.vec1 + Arc.__init__(self, self._xydata, size, size, angle=0.0, theta1=self.theta1, theta2=self.theta2, **kwargs) - Text.__init__(self, x=self._x, y=self._y, text=text, **kwargs) - self.set_transform(IdentityTransform()) - self.ax.add_artist(self) + + if units == "pixels" or units == "points": + textcoords = "offset " + units + else: + textcoords = "offset pixels" + + annotation = Annotation( + text, + self._xydata, + xytext=self._text_pos, + xycoords="data", + textcoords=textcoords, + **kwargs) + + self.ax.add_patch(self) + self.ax.add_artist(annotation) def get_size(self): factor = 1. @@ -91,40 +107,31 @@ def get_theta2(self): def set_theta(self, angle): pass - def get_x_text(self): - return self._xydata[0] + 3*self.size + def get_text_pos(self): + theta = np.deg2rad((self.theta2 + self.theta1)/2) + x = self.width*np.cos(theta) + y = self.height*np.sin(theta) + return (x, y) - def get_y_text(self): - return self._xydata[1] + 3*self.size - - def set_xy_text(self, xy): + def set_text_pos(self, xy): pass - def set_color(self, color): - Arc.set_color(self, color) - Text.set_color(self, color) - - def draw(self, renderer): - Arc.draw(self, renderer) - Text.draw(self, renderer) - _center = property(get_center_in_pixels, set_center) theta1 = property(get_theta1, set_theta) theta2 = property(get_theta2, set_theta) width = property(get_size, set_size) height = property(get_size, set_size) - _x = property(get_x_text, set_xy_text) - _y = property(get_y_text, set_xy_text) + _text_pos = property(get_text_pos, set_text_pos) fig, ax = plt.subplots() -ax.plot([2, .5, 1], [0, .2, 1]) -am = AngleMarker((.5, .2), (2, 0), (1, 1), size=0.25, units="axes max", ax=ax, +ax.plot([2, .5, -1], [1, .2, 1]) +am = AngleMarker((.5, .2), (2, 1), (-1, 1), size=50, units="pixels", ax=ax, text=r"$\theta$") plt.show() -''' + def testing(size=0.25, units="axes fraction", dpi=100, fs=(6.4, 5), show=False): @@ -169,4 +176,3 @@ def plot_angle(ax, pos, vec1, vec2, acol="C0", **kwargs): for (size, unit), dpi, fs in itertools.product(s, d, f): testing(size=size, units=unit, dpi=dpi, fs=fs) -''' From c162b88d6feae4cbc1af7d5b5d38aa88b7c98f4b Mon Sep 17 00:00:00 2001 From: Daniel Ingram Date: Thu, 29 Nov 2018 17:35:51 -0500 Subject: [PATCH 07/14] Separate test from AngleMarker class --- examples/lines_bars_and_markers/AngleArc.py | 68 ------------------- .../lines_bars_and_markers/AngleMarker.py | 59 ++-------------- .../AngleMarker_test.py | 47 +++++++++++++ 3 files changed, 54 insertions(+), 120 deletions(-) delete mode 100644 examples/lines_bars_and_markers/AngleArc.py create mode 100644 examples/lines_bars_and_markers/AngleMarker_test.py diff --git a/examples/lines_bars_and_markers/AngleArc.py b/examples/lines_bars_and_markers/AngleArc.py deleted file mode 100644 index 8fbd4819184f..000000000000 --- a/examples/lines_bars_and_markers/AngleArc.py +++ /dev/null @@ -1,68 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -from matplotlib.patches import Arc -from matplotlib.transforms import IdentityTransform - - -class AngleArc(Arc): - def __init__(self, xy, vec1, vec2, size=100, units="pixels", - ax=None, fig=None, **kwargs): - self._xydata = xy # in data coordinates - self.ax = ax or plt.gca() - self.fig = fig or plt.gcf() - self.vec1 = vec1 # tuple or array of coordinates, relative to xy - self.vec2 = vec2 # tuple or array of coordinates, relative to xy - self.size = size - - super().__init__(self._xydata, size, size, angle=0.0, - theta1=self.theta1, theta2=self.theta2, **kwargs) - - self.set_transform(IdentityTransform()) - - if units == "relative": - fig.canvas.mpl_connect("resize_event", self._resize) - - self.ax.add_patch(self) - - def _resize(self, event): - x0, y0 = self.ax.transAxes.transform((0, 0)) - x1, y1 = self.ax.transAxes.transform((1, 1)) - dx = x1 - x0 - dy = y1 - y0 - smallest = min(dx, dy) - self.width = 0.25*dx - self.height = 0.25*dx - - def get_center_pixels(self): - """ return center in pixel coordinates """ - return self.ax.transData.transform(self._xydata) - - def set_center(self, xy): - """ set center in data coordinates """ - self._xydata = xy - - _center = property(get_center_pixels, set_center) - - def get_theta(self, vec): - vec_in_pixels = self.ax.transData.transform(vec) - self._center - return np.rad2deg(np.arctan2(vec_in_pixels[1], vec_in_pixels[0])) - - def get_theta1(self): - return self.get_theta(self.vec1) - - def get_theta2(self): - return self.get_theta(self.vec2) - - def set_theta(self, angle): - pass - - theta1 = property(get_theta1, set_theta) - theta2 = property(get_theta2, set_theta) - - -fig, ax = plt.subplots() - -ax.plot([2, .5, 1], [0, .2, 1]) -am = AngleArc((0.5, 0.2), (2, 0), (1, 1), size=100, units="relative", - ax=ax, fig=fig) -plt.show() diff --git a/examples/lines_bars_and_markers/AngleMarker.py b/examples/lines_bars_and_markers/AngleMarker.py index f67c864f7910..162366d8cc0d 100644 --- a/examples/lines_bars_and_markers/AngleMarker.py +++ b/examples/lines_bars_and_markers/AngleMarker.py @@ -124,55 +124,10 @@ def set_text_pos(self, xy): _text_pos = property(get_text_pos, set_text_pos) -fig, ax = plt.subplots() - -ax.plot([2, .5, -1], [1, .2, 1]) -am = AngleMarker((.5, .2), (2, 1), (-1, 1), size=50, units="pixels", ax=ax, - text=r"$\theta$") -plt.show() - - -def testing(size=0.25, units="axes fraction", dpi=100, fs=(6.4, 5), - show=False): - - fig, axes = plt.subplots(2, 2, sharex="col", sharey="row", dpi=dpi, - figsize=fs, - gridspec_kw=dict(width_ratios=[1, 3], - height_ratios=[3, 1])) - - def plot_angle(ax, pos, vec1, vec2, acol="C0", **kwargs): - ax.plot([vec1[0], pos[0], vec2[0]], [vec1[1], pos[1], vec2[1]], - color=acol) - am = AngleMarker(pos, vec1, vec2, ax=ax, text=r"$\theta$", **kwargs) - - tx = "figsize={}, dpi={}, arcsize={} {}".format(fs, dpi, size, units) - axes[0, 1].set_title(tx, loc="right", size=9) - kw = dict(size=size, units=units) - p = (.5, .2), (2, 0), (1, 1) - plot_angle(axes[0, 0], *p, **kw) - plot_angle(axes[0, 1], *p, **kw) - plot_angle(axes[1, 1], *p, **kw) - kw.update(acol="limegreen") - plot_angle(axes[0, 0], (1.2, 0), (1, -1), (1.3, -.8), **kw) - plot_angle(axes[1, 1], (0.2, 1), (0, 0), (.3, .2), **kw) - plot_angle(axes[0, 1], (0.2, 0), (0, -1), (.3, -.8), **kw) - kw.update(acol="crimson") - plot_angle(axes[1, 0], (1, .5), (1, 1), (2, .5), **kw) - - fig.tight_layout() - fig.savefig(tx.replace("=", "_") + ".png") - fig.savefig(tx.replace("=", "_") + ".pdf") - if show: - plt.show() - - -s = [(0.25, "axes min"), (0.25, "axes max"), - (0.25, "axes width"), (0.25, "axes height"), - (100, "pixels"), (72, "points")] -d = [72, 144] -f = [(6.4, 5), (12.8, 10)] - -import itertools - -for (size, unit), dpi, fs in itertools.product(s, d, f): - testing(size=size, units=unit, dpi=dpi, fs=fs) +if __name__ == "__main__": + fig, ax = plt.subplots() + + ax.plot([2, .5, -1], [1, .2, 1]) + am = AngleMarker((.5, .2), (2, 1), (-1, 1), size=50, units="pixels", ax=ax, + text=r"$\theta$") + plt.show() diff --git a/examples/lines_bars_and_markers/AngleMarker_test.py b/examples/lines_bars_and_markers/AngleMarker_test.py new file mode 100644 index 000000000000..c62c9771f748 --- /dev/null +++ b/examples/lines_bars_and_markers/AngleMarker_test.py @@ -0,0 +1,47 @@ +import itertools +import matplotlib.pyplot as plt +from AngleMarker import AngleMarker + + +def testing(size=0.25, units="axes fraction", dpi=100, fs=(6.4, 5), + show=False): + + fig, axes = plt.subplots(2, 2, sharex="col", sharey="row", dpi=dpi, + figsize=fs, + gridspec_kw=dict(width_ratios=[1, 3], + height_ratios=[3, 1])) + + def plot_angle(ax, pos, vec1, vec2, acol="C0", **kwargs): + ax.plot([vec1[0], pos[0], vec2[0]], [vec1[1], pos[1], vec2[1]], + color=acol) + am = AngleMarker(pos, vec1, vec2, ax=ax, text=r"$\theta$", **kwargs) + + tx = "figsize={}, dpi={}, arcsize={} {}".format(fs, dpi, size, units) + axes[0, 1].set_title(tx, loc="right", size=9) + kw = dict(size=size, units=units) + p = (.5, .2), (2, 0), (1, 1) + plot_angle(axes[0, 0], *p, **kw) + plot_angle(axes[0, 1], *p, **kw) + plot_angle(axes[1, 1], *p, **kw) + kw.update(acol="limegreen") + plot_angle(axes[0, 0], (1.2, 0), (1, -1), (1.3, -.8), **kw) + plot_angle(axes[1, 1], (0.2, 1), (0, 0), (.3, .2), **kw) + plot_angle(axes[0, 1], (0.2, 0), (0, -1), (.3, -.8), **kw) + kw.update(acol="crimson") + plot_angle(axes[1, 0], (1, .5), (1, 1), (2, .5), **kw) + + fig.tight_layout() + fig.savefig(tx.replace("=", "_") + ".png") + fig.savefig(tx.replace("=", "_") + ".pdf") + if show: + plt.show() + + +s = [(0.25, "axes min"), (0.25, "axes max"), + (0.25, "axes width"), (0.25, "axes height"), + (100, "pixels"), (72, "points")] +d = [72, 144] +f = [(6.4, 5), (12.8, 10)] + +for (size, unit), dpi, fs in itertools.product(s, d, f): + testing(size=size, units=unit, dpi=dpi, fs=fs) From d6113a545424e04f426a912da2460a125df45561 Mon Sep 17 00:00:00 2001 From: ImportanceOfBeingErnest Date: Tue, 17 Sep 2019 05:59:52 +0200 Subject: [PATCH 08/14] finishing angle arc example --- .../lines_bars_and_markers/AngleMarker.py | 133 -------- .../AngleMarker_test.py | 47 --- .../lines_bars_and_markers/angle_marker.py | 323 ++++++++++++++++++ 3 files changed, 323 insertions(+), 180 deletions(-) delete mode 100644 examples/lines_bars_and_markers/AngleMarker.py delete mode 100644 examples/lines_bars_and_markers/AngleMarker_test.py create mode 100644 examples/lines_bars_and_markers/angle_marker.py diff --git a/examples/lines_bars_and_markers/AngleMarker.py b/examples/lines_bars_and_markers/AngleMarker.py deleted file mode 100644 index 162366d8cc0d..000000000000 --- a/examples/lines_bars_and_markers/AngleMarker.py +++ /dev/null @@ -1,133 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -from matplotlib.patches import Arc -from matplotlib.text import Annotation -from matplotlib.transforms import IdentityTransform, TransformedBbox, Bbox - - -class AngleMarker(Arc): - """ - Draws an arc between two vectors which appears circular in display space. - """ - def __init__(self, xy, vec1, vec2, size=100, units="pixels", ax=None, - text='', **kwargs): - """ - Params - ------ - xy, vec1, vec2 : tuple or array of two floats - center position and two points. Angle marker is drawn between the - two vectors connecting vec1 and vec2 with xy, respectively. Units - are data coordinates. - - size : float - diameter of the angle marker in units specified by ``units``. - - units : string - One of the following strings to specify the units of ``size``: - * "pixels" : pixels - * "points" : points, use points instead of pixels to not have a - dependence of 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 marker to - - kwargs : - Further parameters are passed to `matplotlib.patches.Arc`. Use this - to specify, color, linewidth etc of the arc. - - """ - self._xydata = xy # in data coordinates - self.ax = ax or plt.gca() - self.vec1 = vec1 # tuple or array of absolute coordinates - self.vec2 = vec2 # tuple or array of absolute coordinates - self.size = size - self.units = units - - if self.theta1 > self.theta2: - self.vec1, self.vec2 = self.vec2, self.vec1 - - Arc.__init__(self, self._xydata, size, size, angle=0.0, - theta1=self.theta1, theta2=self.theta2, **kwargs) - self.set_transform(IdentityTransform()) - - if units == "pixels" or units == "points": - textcoords = "offset " + units - else: - textcoords = "offset pixels" - - annotation = Annotation( - text, - self._xydata, - xytext=self._text_pos, - xycoords="data", - textcoords=textcoords, - **kwargs) - - self.ax.add_patch(self) - self.ax.add_artist(annotation) - - def get_size(self): - factor = 1. - if self.units == "points": - factor = self.ax.figure.dpi / 72. - elif self.units[:4] == "axes": - b = TransformedBbox(Bbox.from_bounds(0, 0, 1, 1), - self.ax.transAxes) - dic = {"max": max(b.width, b.height), - "min": min(b.width, b.height), - "width": b.width, "height": b.height} - factor = dic[self.units[5:]] - return self.size * factor - - def set_size(self, size): - self.size = size - - def get_center_in_pixels(self): - """ return center in pixels """ - return self.ax.transData.transform(self._xydata) - - def set_center(self, xy): - """ set center in data coordinates """ - self._xydata = xy - - def get_theta(self, vec): - vec_in_pixels = self.ax.transData.transform(vec) - self._center - return np.rad2deg(np.arctan2(vec_in_pixels[1], vec_in_pixels[0])) - - def get_theta1(self): - return self.get_theta(self.vec1) - - def get_theta2(self): - return self.get_theta(self.vec2) - - def set_theta(self, angle): - pass - - def get_text_pos(self): - theta = np.deg2rad((self.theta2 + self.theta1)/2) - x = self.width*np.cos(theta) - y = self.height*np.sin(theta) - return (x, y) - - def set_text_pos(self, xy): - pass - - _center = property(get_center_in_pixels, set_center) - theta1 = property(get_theta1, set_theta) - theta2 = property(get_theta2, set_theta) - width = property(get_size, set_size) - height = property(get_size, set_size) - _text_pos = property(get_text_pos, set_text_pos) - - -if __name__ == "__main__": - fig, ax = plt.subplots() - - ax.plot([2, .5, -1], [1, .2, 1]) - am = AngleMarker((.5, .2), (2, 1), (-1, 1), size=50, units="pixels", ax=ax, - text=r"$\theta$") - plt.show() diff --git a/examples/lines_bars_and_markers/AngleMarker_test.py b/examples/lines_bars_and_markers/AngleMarker_test.py deleted file mode 100644 index c62c9771f748..000000000000 --- a/examples/lines_bars_and_markers/AngleMarker_test.py +++ /dev/null @@ -1,47 +0,0 @@ -import itertools -import matplotlib.pyplot as plt -from AngleMarker import AngleMarker - - -def testing(size=0.25, units="axes fraction", dpi=100, fs=(6.4, 5), - show=False): - - fig, axes = plt.subplots(2, 2, sharex="col", sharey="row", dpi=dpi, - figsize=fs, - gridspec_kw=dict(width_ratios=[1, 3], - height_ratios=[3, 1])) - - def plot_angle(ax, pos, vec1, vec2, acol="C0", **kwargs): - ax.plot([vec1[0], pos[0], vec2[0]], [vec1[1], pos[1], vec2[1]], - color=acol) - am = AngleMarker(pos, vec1, vec2, ax=ax, text=r"$\theta$", **kwargs) - - tx = "figsize={}, dpi={}, arcsize={} {}".format(fs, dpi, size, units) - axes[0, 1].set_title(tx, loc="right", size=9) - kw = dict(size=size, units=units) - p = (.5, .2), (2, 0), (1, 1) - plot_angle(axes[0, 0], *p, **kw) - plot_angle(axes[0, 1], *p, **kw) - plot_angle(axes[1, 1], *p, **kw) - kw.update(acol="limegreen") - plot_angle(axes[0, 0], (1.2, 0), (1, -1), (1.3, -.8), **kw) - plot_angle(axes[1, 1], (0.2, 1), (0, 0), (.3, .2), **kw) - plot_angle(axes[0, 1], (0.2, 0), (0, -1), (.3, -.8), **kw) - kw.update(acol="crimson") - plot_angle(axes[1, 0], (1, .5), (1, 1), (2, .5), **kw) - - fig.tight_layout() - fig.savefig(tx.replace("=", "_") + ".png") - fig.savefig(tx.replace("=", "_") + ".pdf") - if show: - plt.show() - - -s = [(0.25, "axes min"), (0.25, "axes max"), - (0.25, "axes width"), (0.25, "axes height"), - (100, "pixels"), (72, "points")] -d = [72, 144] -f = [(6.4, 5), (12.8, 10)] - -for (size, unit), dpi, fs in itertools.product(s, d, f): - testing(size=size, units=unit, dpi=dpi, fs=fs) diff --git a/examples/lines_bars_and_markers/angle_marker.py b/examples/lines_bars_and_markers/angle_marker.py new file mode 100644 index 000000000000..3728087ce8ee --- /dev/null +++ b/examples/lines_bars_and_markers/angle_marker.py @@ -0,0 +1,323 @@ +""" +============================= +Scale invariant angle marker +============================= + +This example shows how to create a scale invariant angle marker. +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 purpose 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 on the actual +data coordinates - at least if you want to be able to freely zoom into your +plot without the marker 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 ``AngleMarker`` class provides such solution. + +The example below serves two purposes: + +* It provides a read-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 `. + +If mainly interested in the former, you may copy the below class and jump to +the :ref:`angle-marker-usage` section. +""" + +######################################################################### +# AngleMarker class +# ~~~~~~~~~~~~~~~~~ +# The essential idea here is to subclass `~.patches.Arc` and set its transform +# to the `~.transforms.IdentityTransform`. The parameters of the arc are hence +# defined in pixel space. +# We then override the ``Arc``'s attributes ``_center``, +# ``theta1``, ``theta2``, ``width`` and ``height`` and make them properties. +# They are coupled to internal methods that calculate the respective +# parameters each time the attribute is accessed and thereby ensure that the +# arc in pixel space stays synchronized with the input points and size. +# For example, each time the arc's drawing method would query its +# ``_center`` attribute, instead of receiving the same number all over again, +# it will instead receive the result of the ``get_center_in_pixels`` method we +# defined in the subclass. This method transforms the center in data +# coordinates to pixels via the axes transform ``ax.transData``. The +# size and the angles are calculated in a similar fashion, such that the arc +# changes its shape automatically when e.g. zooming or panning interactively. +# +# The functionality of this class allows to annotate the arc with a text. This +# text is a `~.text.Annotation` stored in an attribute ``text``. +# Since the arc's position and radius are defined only at draw time, we need to +# update the text's position accordingly. This is done by reimplementing the +# ``Arc``'s ``draw()`` method to let it call an updating method for the text. +# +# The arc and the text will be added to the provided axes at instantiation: it +# is hence not strictly necessary to keep a reference to it. + + +import numpy as np +import matplotlib +import matplotlib.pyplot as plt +from matplotlib.patches import Arc +from matplotlib.transforms import IdentityTransform, TransformedBbox, Bbox + + +class AngleMarker(Arc): + """ + Draws an arc between two vectors which appears circular in display space. + """ + def __init__(self, xy, p1, p2, size=75, unit="points", ax=None, + text="", textposition="inside", text_kw={}, **kwargs): + """ + Params + ------ + xy, p1, p2 : tuple or array of two floats + center position and two points. Angle marker is drawn between the + two vectors connecting p1 and p2 with xy, respectively. Units + are data coordinates. + + size : float + diameter of the angle marker in units specified by ``unit``. + + unit : string + 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 of 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 marker to + + text : string + 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. + + """ + self.ax = ax or plt.gca() + self._xydata = xy # in data coordinates + self.vec1 = p1 + self.vec2 = p2 + self.size = size + self.unit = unit + self.textposition = textposition + + super().__init__(self._xydata, size, size, angle=0.0, + theta1=self.theta1, theta2=self.theta2, **kwargs) + + self.set_transform(IdentityTransform()) + self.ax.add_patch(self) + + self.kw = dict(ha="center", va="center", + xycoords=IdentityTransform(), xytext=(0, 0), + textcoords="offset points") + self.kw.update(text_kw) + self.text = ax.annotate(text, xy=self._center, + **self.kw) + + def get_size(self): + factor = 1. + if self.unit == "points": + factor = self.ax.figure.dpi / 72. + elif self.unit[:4] == "axes": + b = TransformedBbox(Bbox.from_bounds(0, 0, 1, 1), + self.ax.transAxes) + dic = {"max": max(b.width, b.height), + "min": min(b.width, b.height), + "width": b.width, "height": b.height} + factor = dic[self.unit[5:]] + return self.size * factor + + def set_size(self, size): + self.size = size + + def get_center_in_pixels(self): + """return center in pixels""" + return self.ax.transData.transform(self._xydata) + + def set_center(self, xy): + """set center in data coordinates""" + self._xydata = xy + + def get_theta(self, vec): + vec_in_pixels = self.ax.transData.transform(vec) - self._center + return np.rad2deg(np.arctan2(vec_in_pixels[1], vec_in_pixels[0])) + + def get_theta1(self): + return self.get_theta(self.vec1) + + def get_theta2(self): + return self.get_theta(self.vec2) + + def set_theta(self, angle): + pass + + # Redefine attributes of the Arc to always give values in pixel space + _center = property(get_center_in_pixels, set_center) + theta1 = property(get_theta1, set_theta) + theta2 = property(get_theta2, set_theta) + width = property(get_size, set_size) + height = property(get_size, set_size) + + # The following two methods are needed to update the text position. + def draw(self, renderer): + self.update_text() + super().draw(renderer) + + def update_text(self): + c = self._center + s = self.get_size() + angle_span = (self.theta2 - self.theta1) % 360 + angle = np.deg2rad(self.theta1 + angle_span / 2) + r = s / 2 + if self.textposition == "inside": + r = s / np.interp(angle_span, [60, 90, 135, 180], + [3.3, 3.5, 3.8, 4]) + self.text.xy = c + r * np.array([np.cos(angle), np.sin(angle)]) + if self.textposition == "outside": + def R90(a, r, w, h): + if a < np.arctan(h/2/(r+w/2)): + return np.sqrt((r+w/2)**2 + (np.tan(a)*(r+w/2))**2) + else: + c = np.sqrt((w/2)**2+(h/2)**2) + T = np.arcsin(c * np.cos(np.pi/2 - a + np.arcsin(h/2/c))/r) + xy = r * np.array([np.cos(a + T), np.sin(a + T)]) + xy += np.array([w/2, h/2]) + return np.sqrt(np.sum(xy**2)) + + def R(a, r, w, h): + aa = (a % (np.pi/4))*((a % (np.pi/2)) <= np.pi/4) + \ + (np.pi/4 - (a % (np.pi/4)))*((a % (np.pi/2)) >= np.pi/4) + return R90(aa, r, *[w, h][::int(np.sign(np.cos(2*a)))]) + + bbox = self.text.get_window_extent() + X = R(angle, r, bbox.width, bbox.height) + trans = self.ax.figure.dpi_scale_trans.inverted() + offs = trans.transform(((X-s/2), 0))[0] * 72 + self.text.set_position([offs*np.cos(angle), offs*np.sin(angle)]) + + +######################################################################### +# .. _angle-marker-usage: +# +# Usage +# ~~~~~ +# +# Required arguments to ``AngleMarker`` are the center of the arc, ``xy``, +# and two points, such that the arc spans between the two vectors +# connecting p1 and p2 with xy, respectively. Those are given in data +# coordinates. +# Further arguments are the ``size`` of the arc and its ``unit``. +# Additionally, a ``text`` can be specified, that will be drawn either in- or +# outside of the arc, according to the value of ``textposition``. +# Usage of those arguments is shown below. + +fig, (ax1, ax2, ax3) = plt.subplots(nrows=3, sharex=True, figsize=(7, 7), + gridspec_kw=dict(height_ratios=(3, 1, 1))) +ax2.margins(y=0.4) +ax3.margins(y=0.4) +fig.canvas.draw() # Need to draw the figure to define renderer + +#### SUBPLOT 1 #### +# Plot two crossing lines and label each angle between them with the +# above `AngleMarker`tool. +center = (4.5, 650) +p1 = [(2.5, 710), (6.0, 605)] +p2 = [(3.0, 275), (5.5, 900)] +line1, = ax1.plot(*zip(*p1)) +line2, = ax1.plot(*zip(*p2)) +point, = ax1.plot(*center, marker="o") + +am1 = AngleMarker(center, p1[1], p2[1], ax=ax1, size=75, text=r"$\alpha$") +am2 = AngleMarker(center, p2[1], p1[0], ax=ax1, size=35, text=r"$\beta$") +am3 = AngleMarker(center, p1[0], p2[0], ax=ax1, size=75, text=r"$\gamma$") +am4 = AngleMarker(center, p2[0], p1[1], ax=ax1, size=35, text=r"$\theta$") + + +# Showcase some styling options for the angle arc, as well as the text +p = [(6.0, 400), (5.3, 410), (5.6, 300)] +ax1.plot(*zip(*p)) +am5 = AngleMarker(p[1], p[0], p[2], ax=ax1, size=40, text=r"$\Phi$", + linestyle="--", color="gray", textposition="outside", + text_kw=dict(fontsize=16, color="gray")) + + +#### SUBPLOT 2 #### +# Helper function to draw angle easily +def plot_angle(ax, pos, angle, length=0.95, acol="C0", **kwargs): + vec2 = np.array([np.cos(np.deg2rad(angle)), np.sin(np.deg2rad(angle))]) + xy = np.c_[[length, 0], [0, 0], vec2*length].T + np.array(pos) + ax.plot(*xy.T, color=acol) + return AngleMarker(pos, xy[0], xy[2], ax=ax, **kwargs) + +# Showcase different textpositions +kw = dict(size=75, unit="points", text=r"$60°$") + +am6 = plot_angle(ax2, (2.0, 0), 60, textposition="inside", **kw) +am7 = plot_angle(ax2, (3.5, 0), 60, textposition="outside", **kw) +am8 = plot_angle(ax2, (5.0, 0), 60, textposition="edge", + text_kw=dict(bbox=dict(boxstyle="round", fc="w")), **kw) +am9 = plot_angle(ax2, (6.5, 0), 60, textposition="edge", + text_kw=dict(xytext=(30, 20), arrowprops=dict(arrowstyle="->", + connectionstyle="arc3,rad=-0.2")), **kw) + +ax2.annotate("textpostion", xy=(.02, .9), xycoords="axes fraction", + bbox=dict(boxstyle="round", fc="w"), ha="left") +for x, text in zip([2.0, 3.5, 5.0, 6.5], ['"inside"', '"outside"', '"edge"', + '"edge", custom arrow']): + ax2.annotate(text, xy=(x, 0), xycoords=ax2.get_xaxis_transform(), + bbox=dict(boxstyle="round", fc="w"), ha="left", fontsize=8) + +#### SUBPLOT 3 #### +# Showcase different size units. The effect of this can best be observed +# by interactively changing the figure size +kw = dict(text=r"$60°$", textposition="outside") + +am10 = plot_angle(ax3, (2.0, 0), 60, size=50, unit="pixels", **kw) +am11 = plot_angle(ax3, (3.5, 0), 60, size=50, unit="points", **kw) +am12 = plot_angle(ax3, (5.0, 0), 60, size=0.25, unit="axes min", **kw) +am13 = plot_angle(ax3, (6.5, 0), 60, size=0.25, unit="axes max", **kw) + +ax3.annotate("unit", xy=(.02, .9), xycoords="axes fraction", + bbox=dict(boxstyle="round", fc="w"), ha="left") +for x, text in zip([2.0, 3.5, 5.0, 6.5], ['"pixels"', '"points"', + '"axes min"', '"axes max"']): + ax3.annotate(text, xy=(x, 0), xycoords=ax3.get_xaxis_transform(), + bbox=dict(boxstyle="round", fc="w"), ha="left", fontsize=8) + +fig.tight_layout() +plt.show() + + +############################################################################# +# +# ------------ +# +# References +# """""""""" +# +# The use of the following functions, methods and classes is shown +# in this example: + +matplotlib.patches.Arc +matplotlib.axes.Axes.annotate +matplotlib.pyplot.annotate +matplotlib.text.Annotation +matplotlib.transforms.IdentityTransform +matplotlib.transforms.TransformedBbox +matplotlib.transforms.Bbox From 32ef7f873bdad22e4679963bfc660c2d64cda128 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 8 Sep 2020 20:42:30 -0400 Subject: [PATCH 09/14] Clean up prose in invariant angle marker example. Also, simplify locating some annotations. --- .../lines_bars_and_markers/angle_marker.py | 132 +++++++++--------- 1 file changed, 65 insertions(+), 67 deletions(-) diff --git a/examples/lines_bars_and_markers/angle_marker.py b/examples/lines_bars_and_markers/angle_marker.py index 3728087ce8ee..03af37778912 100644 --- a/examples/lines_bars_and_markers/angle_marker.py +++ b/examples/lines_bars_and_markers/angle_marker.py @@ -1,28 +1,28 @@ """ -============================= +============================ Scale invariant angle marker -============================= +============================ -This example shows how to create a scale invariant angle marker. -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 purpose 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 on the actual -data coordinates - at least if you want to be able to freely zoom into your -plot without the marker growing to infinity. +This example shows how to create a scale invariant angle marker. 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 marker 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 ``AngleMarker`` class provides such solution. +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 ``AngleMarker`` class provides such solution. The example below serves two purposes: -* It provides a read-to-use solution for the problem of easily drawing angles +* 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 `. +* 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 `. If mainly interested in the former, you may copy the below class and jump to the :ref:`angle-marker-usage` section. @@ -32,28 +32,28 @@ # AngleMarker class # ~~~~~~~~~~~~~~~~~ # The essential idea here is to subclass `~.patches.Arc` and set its transform -# to the `~.transforms.IdentityTransform`. The parameters of the arc are hence +# to the `~.transforms.IdentityTransform`, making the parameters of the arc # defined in pixel space. -# We then override the ``Arc``'s attributes ``_center``, -# ``theta1``, ``theta2``, ``width`` and ``height`` and make them properties. -# They are coupled to internal methods that calculate the respective -# parameters each time the attribute is accessed and thereby ensure that the -# arc in pixel space stays synchronized with the input points and size. -# For example, each time the arc's drawing method would query its -# ``_center`` attribute, instead of receiving the same number all over again, -# it will instead receive the result of the ``get_center_in_pixels`` method we -# defined in the subclass. This method transforms the center in data -# coordinates to pixels via the axes transform ``ax.transData``. The -# size and the angles are calculated in a similar fashion, such that the arc -# changes its shape automatically when e.g. zooming or panning interactively. +# We then override the ``Arc``'s attributes ``_center``, ``theta1``, +# ``theta2``, ``width`` and ``height`` and make them properties, coupling to +# internal methods that calculate the respective parameters each time the +# attribute is accessed and thereby ensuring that the arc in pixel space stays +# synchronized with the input points and size. +# For example, each time the arc's drawing method would query its ``_center`` +# attribute, instead of receiving the same number all over again, it will +# instead receive the result of the ``get_center_in_pixels`` method we defined +# in the subclass. This method transforms the center in data coordinates to +# pixels via the Axes transform ``ax.transData``. The size and the angles are +# calculated in a similar fashion, such that the arc changes its shape +# automatically when e.g. zooming or panning interactively. # # The functionality of this class allows to annotate the arc with a text. This -# text is a `~.text.Annotation` stored in an attribute ``text``. -# Since the arc's position and radius are defined only at draw time, we need to -# update the text's position accordingly. This is done by reimplementing the -# ``Arc``'s ``draw()`` method to let it call an updating method for the text. +# text is a `~.text.Annotation` stored in an attribute ``text``. Since the +# arc's position and radius are defined only at draw time, we need to update +# the text's position accordingly. This is done by reimplementing the ``Arc``'s +# ``draw()`` method to let it call an updating method for the text. # -# The arc and the text will be added to the provided axes at instantiation: it +# The arc and the text will be added to the provided Axes at instantiation: it # is hence not strictly necessary to keep a reference to it. @@ -71,40 +71,40 @@ class AngleMarker(Arc): def __init__(self, xy, p1, p2, size=75, unit="points", ax=None, text="", textposition="inside", text_kw={}, **kwargs): """ - Params - ------ + Parameters + ---------- xy, p1, p2 : tuple or array of two floats - center position and two points. Angle marker is drawn between the - two vectors connecting p1 and p2 with xy, respectively. Units + Center position and two points. Angle marker is drawn between the + two vectors connecting *p1* and *p2* with *xy*, respectively. Units are data coordinates. size : float - diameter of the angle marker in units specified by ``unit``. + Diameter of the angle marker in units specified by *unit*. unit : string - 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 of the dpi - * "axes width", "axes height" : relative units of axes - width, height - * "axes min", "axes max" : minimum or maximum of relative axes - width, height + 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 marker to + The Axes to add the angle marker to. text : string The text to mark the angle with. - textposition : ["inside", "outside", "edge"] + 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 : + **kwargs Further parameters are passed to `matplotlib.patches.Arc`. Use this to specify, color, linewidth etc. of the arc. @@ -218,14 +218,13 @@ def R(a, r, w, h): # Usage # ~~~~~ # -# Required arguments to ``AngleMarker`` are the center of the arc, ``xy``, -# and two points, such that the arc spans between the two vectors -# connecting p1 and p2 with xy, respectively. Those are given in data -# coordinates. -# Further arguments are the ``size`` of the arc and its ``unit``. -# Additionally, a ``text`` can be specified, that will be drawn either in- or -# outside of the arc, according to the value of ``textposition``. -# Usage of those arguments is shown below. +# Required arguments to ``AngleMarker`` are the center of the arc, *xy*, and +# two points, such that the arc spans between the two vectors connecting *p1* +# and *p2* with *xy*, respectively. Those are given in data coordinates. +# Further arguments are the *size* of the arc and its *unit*. Additionally, a +# *text* can be specified, that will be drawn either in- or outside of the arc, +# according to the value of *textposition*. Usage of those arguments is shown +# below. fig, (ax1, ax2, ax3) = plt.subplots(nrows=3, sharex=True, figsize=(7, 7), gridspec_kw=dict(height_ratios=(3, 1, 1))) @@ -249,7 +248,7 @@ def R(a, r, w, h): am4 = AngleMarker(center, p2[0], p1[1], ax=ax1, size=35, text=r"$\theta$") -# Showcase some styling options for the angle arc, as well as the text +# Showcase some styling options for the angle arc, as well as the text. p = [(6.0, 400), (5.3, 410), (5.6, 300)] ax1.plot(*zip(*p)) am5 = AngleMarker(p[1], p[0], p[2], ax=ax1, size=40, text=r"$\Phi$", @@ -258,14 +257,14 @@ def R(a, r, w, h): #### SUBPLOT 2 #### -# Helper function to draw angle easily +# Helper function to draw angle easily. def plot_angle(ax, pos, angle, length=0.95, acol="C0", **kwargs): vec2 = np.array([np.cos(np.deg2rad(angle)), np.sin(np.deg2rad(angle))]) xy = np.c_[[length, 0], [0, 0], vec2*length].T + np.array(pos) ax.plot(*xy.T, color=acol) return AngleMarker(pos, xy[0], xy[2], ax=ax, **kwargs) -# Showcase different textpositions +# Showcase different text positions. kw = dict(size=75, unit="points", text=r"$60°$") am6 = plot_angle(ax2, (2.0, 0), 60, textposition="inside", **kw) @@ -276,8 +275,8 @@ def plot_angle(ax, pos, angle, length=0.95, acol="C0", **kwargs): text_kw=dict(xytext=(30, 20), arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=-0.2")), **kw) -ax2.annotate("textpostion", xy=(.02, .9), xycoords="axes fraction", - bbox=dict(boxstyle="round", fc="w"), ha="left") +ax2.annotate("textposition", xy=(.02, 1), xycoords="axes fraction", + bbox=dict(boxstyle="round", fc="w"), ha="left", va="center") for x, text in zip([2.0, 3.5, 5.0, 6.5], ['"inside"', '"outside"', '"edge"', '"edge", custom arrow']): ax2.annotate(text, xy=(x, 0), xycoords=ax2.get_xaxis_transform(), @@ -293,14 +292,13 @@ def plot_angle(ax, pos, angle, length=0.95, acol="C0", **kwargs): am12 = plot_angle(ax3, (5.0, 0), 60, size=0.25, unit="axes min", **kw) am13 = plot_angle(ax3, (6.5, 0), 60, size=0.25, unit="axes max", **kw) -ax3.annotate("unit", xy=(.02, .9), xycoords="axes fraction", - bbox=dict(boxstyle="round", fc="w"), ha="left") +ax3.annotate("unit", xy=(.02, 1), xycoords="axes fraction", + bbox=dict(boxstyle="round", fc="w"), ha="left", va="center") for x, text in zip([2.0, 3.5, 5.0, 6.5], ['"pixels"', '"points"', '"axes min"', '"axes max"']): ax3.annotate(text, xy=(x, 0), xycoords=ax3.get_xaxis_transform(), bbox=dict(boxstyle="round", fc="w"), ha="left", fontsize=8) -fig.tight_layout() plt.show() From 8ab46f5de94ab703cbf981e2e555533cb707a763 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 8 Sep 2020 20:53:36 -0400 Subject: [PATCH 10/14] Enable annotation clipping on invariant angle example. Otherwise, panning around leaves the angle text visible even outside the Axes. --- .../lines_bars_and_markers/angle_marker.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/examples/lines_bars_and_markers/angle_marker.py b/examples/lines_bars_and_markers/angle_marker.py index 03af37778912..c8cdebd2cf71 100644 --- a/examples/lines_bars_and_markers/angle_marker.py +++ b/examples/lines_bars_and_markers/angle_marker.py @@ -69,7 +69,7 @@ class AngleMarker(Arc): Draws an arc between two vectors which appears circular in display space. """ def __init__(self, xy, p1, p2, size=75, unit="points", ax=None, - text="", textposition="inside", text_kw={}, **kwargs): + text="", textposition="inside", text_kw=None, **kwargs): """ Parameters ---------- @@ -124,11 +124,11 @@ def __init__(self, xy, p1, p2, size=75, unit="points", ax=None, self.ax.add_patch(self) self.kw = dict(ha="center", va="center", - xycoords=IdentityTransform(), xytext=(0, 0), - textcoords="offset points") - self.kw.update(text_kw) - self.text = ax.annotate(text, xy=self._center, - **self.kw) + xycoords=IdentityTransform(), + xytext=(0, 0), textcoords="offset points", + annotation_clip=True) + self.kw.update(text_kw or {}) + self.text = ax.annotate(text, xy=self._center, **self.kw) def get_size(self): factor = 1. @@ -280,7 +280,8 @@ def plot_angle(ax, pos, angle, length=0.95, acol="C0", **kwargs): for x, text in zip([2.0, 3.5, 5.0, 6.5], ['"inside"', '"outside"', '"edge"', '"edge", custom arrow']): ax2.annotate(text, xy=(x, 0), xycoords=ax2.get_xaxis_transform(), - bbox=dict(boxstyle="round", fc="w"), ha="left", fontsize=8) + bbox=dict(boxstyle="round", fc="w"), ha="left", fontsize=8, + annotation_clip=True) #### SUBPLOT 3 #### # Showcase different size units. The effect of this can best be observed @@ -297,7 +298,8 @@ def plot_angle(ax, pos, angle, length=0.95, acol="C0", **kwargs): for x, text in zip([2.0, 3.5, 5.0, 6.5], ['"pixels"', '"points"', '"axes min"', '"axes max"']): ax3.annotate(text, xy=(x, 0), xycoords=ax3.get_xaxis_transform(), - bbox=dict(boxstyle="round", fc="w"), ha="left", fontsize=8) + bbox=dict(boxstyle="round", fc="w"), ha="left", fontsize=8, + annotation_clip=True) plt.show() From 1e5f172e388d2e17972fb95bc5a92b4330c9f2cd Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 10 Sep 2020 23:41:01 -0400 Subject: [PATCH 11/14] Fix types. --- examples/lines_bars_and_markers/angle_marker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/lines_bars_and_markers/angle_marker.py b/examples/lines_bars_and_markers/angle_marker.py index c8cdebd2cf71..44670e41b728 100644 --- a/examples/lines_bars_and_markers/angle_marker.py +++ b/examples/lines_bars_and_markers/angle_marker.py @@ -81,7 +81,7 @@ def __init__(self, xy, p1, p2, size=75, unit="points", ax=None, size : float Diameter of the angle marker in units specified by *unit*. - unit : string + unit : str One of the following strings to specify the unit of *size*: * "pixels": pixels @@ -94,7 +94,7 @@ def __init__(self, xy, p1, p2, size=75, unit="points", ax=None, ax : `matplotlib.axes.Axes` The Axes to add the angle marker to. - text : string + text : str The text to mark the angle with. textposition : {"inside", "outside", "edge"} From 363bc5ef95a8a2caa2b47d77c62d4121efea9e82 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 10 Sep 2020 23:47:33 -0400 Subject: [PATCH 12/14] Rename angle marker example to angle annotation. --- .../angle_annotation.py} | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) rename examples/{lines_bars_and_markers/angle_marker.py => text_labels_and_annotations/angle_annotation.py} (91%) diff --git a/examples/lines_bars_and_markers/angle_marker.py b/examples/text_labels_and_annotations/angle_annotation.py similarity index 91% rename from examples/lines_bars_and_markers/angle_marker.py rename to examples/text_labels_and_annotations/angle_annotation.py index 44670e41b728..e82fbe2082c9 100644 --- a/examples/lines_bars_and_markers/angle_marker.py +++ b/examples/text_labels_and_annotations/angle_annotation.py @@ -1,7 +1,7 @@ """ -============================ -Scale invariant angle marker -============================ +=========================== +Scale invariant angle label +=========================== This example shows how to create a scale invariant angle marker. It is often useful to mark angles between lines or inside shapes with a circular arc. While @@ -14,7 +14,7 @@ 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 ``AngleMarker`` class provides such solution. +dimension. The following ``AngleAnnotation`` class provides such solution. The example below serves two purposes: @@ -29,8 +29,8 @@ """ ######################################################################### -# AngleMarker class -# ~~~~~~~~~~~~~~~~~ +# AngleAnnotation class +# ~~~~~~~~~~~~~~~~~~~~~ # The essential idea here is to subclass `~.patches.Arc` and set its transform # to the `~.transforms.IdentityTransform`, making the parameters of the arc # defined in pixel space. @@ -64,7 +64,7 @@ from matplotlib.transforms import IdentityTransform, TransformedBbox, Bbox -class AngleMarker(Arc): +class AngleAnnotation(Arc): """ Draws an arc between two vectors which appears circular in display space. """ @@ -218,9 +218,9 @@ def R(a, r, w, h): # Usage # ~~~~~ # -# Required arguments to ``AngleMarker`` are the center of the arc, *xy*, and -# two points, such that the arc spans between the two vectors connecting *p1* -# and *p2* with *xy*, respectively. Those are given in data coordinates. +# Required arguments to ``AngleAnnotation`` are the center of the arc, *xy*, +# and two points, such that the arc spans between the two vectors connecting +# *p1* and *p2* with *xy*, respectively. Those are given in data coordinates. # Further arguments are the *size* of the arc and its *unit*. Additionally, a # *text* can be specified, that will be drawn either in- or outside of the arc, # according to the value of *textposition*. Usage of those arguments is shown @@ -233,8 +233,8 @@ def R(a, r, w, h): fig.canvas.draw() # Need to draw the figure to define renderer #### SUBPLOT 1 #### -# Plot two crossing lines and label each angle between them with the -# above `AngleMarker`tool. +# Plot two crossing lines and label each angle between them with the above +# ``AngleAnnotation`` tool. center = (4.5, 650) p1 = [(2.5, 710), (6.0, 605)] p2 = [(3.0, 275), (5.5, 900)] @@ -242,18 +242,18 @@ def R(a, r, w, h): line2, = ax1.plot(*zip(*p2)) point, = ax1.plot(*center, marker="o") -am1 = AngleMarker(center, p1[1], p2[1], ax=ax1, size=75, text=r"$\alpha$") -am2 = AngleMarker(center, p2[1], p1[0], ax=ax1, size=35, text=r"$\beta$") -am3 = AngleMarker(center, p1[0], p2[0], ax=ax1, size=75, text=r"$\gamma$") -am4 = AngleMarker(center, p2[0], p1[1], ax=ax1, size=35, text=r"$\theta$") +am1 = AngleAnnotation(center, p1[1], p2[1], ax=ax1, size=75, text=r"$\alpha$") +am2 = AngleAnnotation(center, p2[1], p1[0], ax=ax1, size=35, text=r"$\beta$") +am3 = AngleAnnotation(center, p1[0], p2[0], ax=ax1, size=75, text=r"$\gamma$") +am4 = AngleAnnotation(center, p2[0], p1[1], ax=ax1, size=35, text=r"$\theta$") # Showcase some styling options for the angle arc, as well as the text. p = [(6.0, 400), (5.3, 410), (5.6, 300)] ax1.plot(*zip(*p)) -am5 = AngleMarker(p[1], p[0], p[2], ax=ax1, size=40, text=r"$\Phi$", - linestyle="--", color="gray", textposition="outside", - text_kw=dict(fontsize=16, color="gray")) +am5 = AngleAnnotation(p[1], p[0], p[2], ax=ax1, size=40, text=r"$\Phi$", + linestyle="--", color="gray", textposition="outside", + text_kw=dict(fontsize=16, color="gray")) #### SUBPLOT 2 #### @@ -262,7 +262,7 @@ def plot_angle(ax, pos, angle, length=0.95, acol="C0", **kwargs): vec2 = np.array([np.cos(np.deg2rad(angle)), np.sin(np.deg2rad(angle))]) xy = np.c_[[length, 0], [0, 0], vec2*length].T + np.array(pos) ax.plot(*xy.T, color=acol) - return AngleMarker(pos, xy[0], xy[2], ax=ax, **kwargs) + return AngleAnnotation(pos, xy[0], xy[2], ax=ax, **kwargs) # Showcase different text positions. kw = dict(size=75, unit="points", text=r"$60°$") From ecf51a54e0aa599b60216e4bcd7a3014be5ef072 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 11 Sep 2020 01:09:34 -0400 Subject: [PATCH 13/14] Split angle annotation example figure in two. --- .../angle_annotation.py | 68 +++++++++++-------- 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/examples/text_labels_and_annotations/angle_annotation.py b/examples/text_labels_and_annotations/angle_annotation.py index e82fbe2082c9..d8f98cf24857 100644 --- a/examples/text_labels_and_annotations/angle_annotation.py +++ b/examples/text_labels_and_annotations/angle_annotation.py @@ -226,37 +226,41 @@ def R(a, r, w, h): # according to the value of *textposition*. Usage of those arguments is shown # below. -fig, (ax1, ax2, ax3) = plt.subplots(nrows=3, sharex=True, figsize=(7, 7), - gridspec_kw=dict(height_ratios=(3, 1, 1))) -ax2.margins(y=0.4) -ax3.margins(y=0.4) +fig, ax = plt.subplots() fig.canvas.draw() # Need to draw the figure to define renderer +ax.set_title("AngleLabel example") -#### SUBPLOT 1 #### # Plot two crossing lines and label each angle between them with the above # ``AngleAnnotation`` tool. center = (4.5, 650) p1 = [(2.5, 710), (6.0, 605)] p2 = [(3.0, 275), (5.5, 900)] -line1, = ax1.plot(*zip(*p1)) -line2, = ax1.plot(*zip(*p2)) -point, = ax1.plot(*center, marker="o") +line1, = ax.plot(*zip(*p1)) +line2, = ax.plot(*zip(*p2)) +point, = ax.plot(*center, marker="o") -am1 = AngleAnnotation(center, p1[1], p2[1], ax=ax1, size=75, text=r"$\alpha$") -am2 = AngleAnnotation(center, p2[1], p1[0], ax=ax1, size=35, text=r"$\beta$") -am3 = AngleAnnotation(center, p1[0], p2[0], ax=ax1, size=75, text=r"$\gamma$") -am4 = AngleAnnotation(center, p2[0], p1[1], ax=ax1, size=35, text=r"$\theta$") +am1 = AngleAnnotation(center, p1[1], p2[1], ax=ax, size=75, text=r"$\alpha$") +am2 = AngleAnnotation(center, p2[1], p1[0], ax=ax, size=35, text=r"$\beta$") +am3 = AngleAnnotation(center, p1[0], p2[0], ax=ax, size=75, text=r"$\gamma$") +am4 = AngleAnnotation(center, p2[0], p1[1], ax=ax, size=35, text=r"$\theta$") # Showcase some styling options for the angle arc, as well as the text. p = [(6.0, 400), (5.3, 410), (5.6, 300)] -ax1.plot(*zip(*p)) -am5 = AngleAnnotation(p[1], p[0], p[2], ax=ax1, size=40, text=r"$\Phi$", +ax.plot(*zip(*p)) +am5 = AngleAnnotation(p[1], p[0], p[2], ax=ax, size=40, text=r"$\Phi$", linestyle="--", color="gray", textposition="outside", text_kw=dict(fontsize=16, color="gray")) -#### SUBPLOT 2 #### +######################################################################### +# ``AngleLabel`` options +# ~~~~~~~~~~~~~~~~~~~~~~ +# +# The *textposition* and *unit* keyword arguments may be used to modify the +# location of the text label, as shown below: + + # Helper function to draw angle easily. def plot_angle(ax, pos, angle, length=0.95, acol="C0", **kwargs): vec2 = np.array([np.cos(np.deg2rad(angle)), np.sin(np.deg2rad(angle))]) @@ -264,40 +268,44 @@ def plot_angle(ax, pos, angle, length=0.95, acol="C0", **kwargs): ax.plot(*xy.T, color=acol) return AngleAnnotation(pos, xy[0], xy[2], ax=ax, **kwargs) + +fig, (ax1, ax2) = plt.subplots(nrows=2, sharex=True) +fig.suptitle("AngleLabel keyword arguments") +fig.canvas.draw() # Need to draw the figure to define renderer + # Showcase different text positions. +ax1.margins(y=0.4) +ax1.set_title("textposition") kw = dict(size=75, unit="points", text=r"$60°$") -am6 = plot_angle(ax2, (2.0, 0), 60, textposition="inside", **kw) -am7 = plot_angle(ax2, (3.5, 0), 60, textposition="outside", **kw) -am8 = plot_angle(ax2, (5.0, 0), 60, textposition="edge", +am6 = plot_angle(ax1, (2.0, 0), 60, textposition="inside", **kw) +am7 = plot_angle(ax1, (3.5, 0), 60, textposition="outside", **kw) +am8 = plot_angle(ax1, (5.0, 0), 60, textposition="edge", text_kw=dict(bbox=dict(boxstyle="round", fc="w")), **kw) -am9 = plot_angle(ax2, (6.5, 0), 60, textposition="edge", +am9 = plot_angle(ax1, (6.5, 0), 60, textposition="edge", text_kw=dict(xytext=(30, 20), arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=-0.2")), **kw) -ax2.annotate("textposition", xy=(.02, 1), xycoords="axes fraction", - bbox=dict(boxstyle="round", fc="w"), ha="left", va="center") for x, text in zip([2.0, 3.5, 5.0, 6.5], ['"inside"', '"outside"', '"edge"', '"edge", custom arrow']): - ax2.annotate(text, xy=(x, 0), xycoords=ax2.get_xaxis_transform(), + ax1.annotate(text, xy=(x, 0), xycoords=ax1.get_xaxis_transform(), bbox=dict(boxstyle="round", fc="w"), ha="left", fontsize=8, annotation_clip=True) -#### SUBPLOT 3 #### # Showcase different size units. The effect of this can best be observed # by interactively changing the figure size +ax2.margins(y=0.4) +ax2.set_title("unit") kw = dict(text=r"$60°$", textposition="outside") -am10 = plot_angle(ax3, (2.0, 0), 60, size=50, unit="pixels", **kw) -am11 = plot_angle(ax3, (3.5, 0), 60, size=50, unit="points", **kw) -am12 = plot_angle(ax3, (5.0, 0), 60, size=0.25, unit="axes min", **kw) -am13 = plot_angle(ax3, (6.5, 0), 60, size=0.25, unit="axes max", **kw) +am10 = plot_angle(ax2, (2.0, 0), 60, size=50, unit="pixels", **kw) +am11 = plot_angle(ax2, (3.5, 0), 60, size=50, unit="points", **kw) +am12 = plot_angle(ax2, (5.0, 0), 60, size=0.25, unit="axes min", **kw) +am13 = plot_angle(ax2, (6.5, 0), 60, size=0.25, unit="axes max", **kw) -ax3.annotate("unit", xy=(.02, 1), xycoords="axes fraction", - bbox=dict(boxstyle="round", fc="w"), ha="left", va="center") for x, text in zip([2.0, 3.5, 5.0, 6.5], ['"pixels"', '"points"', '"axes min"', '"axes max"']): - ax3.annotate(text, xy=(x, 0), xycoords=ax3.get_xaxis_transform(), + ax2.annotate(text, xy=(x, 0), xycoords=ax2.get_xaxis_transform(), bbox=dict(boxstyle="round", fc="w"), ha="left", fontsize=8, annotation_clip=True) From e25977169ef994a6aeff60faf1bf23483be44941 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 11 Sep 2020 01:48:34 -0400 Subject: [PATCH 14/14] Fix a few more marker -> annotation. --- .../angle_annotation.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/examples/text_labels_and_annotations/angle_annotation.py b/examples/text_labels_and_annotations/angle_annotation.py index d8f98cf24857..d22501232ce2 100644 --- a/examples/text_labels_and_annotations/angle_annotation.py +++ b/examples/text_labels_and_annotations/angle_annotation.py @@ -3,14 +3,14 @@ Scale invariant angle label =========================== -This example shows how to create a scale invariant angle marker. 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 +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 marker growing to infinity. +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 @@ -25,7 +25,7 @@ system `. If mainly interested in the former, you may copy the below class and jump to -the :ref:`angle-marker-usage` section. +the :ref:`angle-annotation-usage` section. """ ######################################################################### @@ -74,12 +74,12 @@ def __init__(self, xy, p1, p2, size=75, unit="points", ax=None, Parameters ---------- xy, p1, p2 : tuple or array of two floats - Center position and two points. Angle marker is drawn between the - two vectors connecting *p1* and *p2* with *xy*, respectively. Units - are data coordinates. + 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 marker in units specified by *unit*. + Diameter of the angle annotation in units specified by *unit*. unit : str One of the following strings to specify the unit of *size*: @@ -92,7 +92,7 @@ def __init__(self, xy, p1, p2, size=75, unit="points", ax=None, width, height ax : `matplotlib.axes.Axes` - The Axes to add the angle marker to. + The Axes to add the angle annotation to. text : str The text to mark the angle with. @@ -213,7 +213,7 @@ def R(a, r, w, h): ######################################################################### -# .. _angle-marker-usage: +# .. _angle-annotation-usage: # # Usage # ~~~~~