diff --git a/doc/api/next_api_changes/behavior/18794-ES.rst b/doc/api/next_api_changes/behavior/18794-ES.rst new file mode 100644 index 000000000000..43c9d9fb9231 --- /dev/null +++ b/doc/api/next_api_changes/behavior/18794-ES.rst @@ -0,0 +1,20 @@ +``QuiverKey`` properties are now modifiable +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The `.QuiverKey` object returned by `.pyplot.quiverkey` and `.axes.Axes.quiverkey` +formerly saved various properties as attributes during initialization. However, +modifying these attributes may or may not have had an effect on the final result. + +Now all such properties have getters and setters, and may be modified after creation: + +- `.QuiverKey.X` -> `.QuiverKey.get_x` / `.QuiverKey.set_x` / + `.QuiverKey.get_position` / `.QuiverKey.set_position` +- `.QuiverKey.Y` -> `.QuiverKey.get_y` / `.QuiverKey.set_y` / + `.QuiverKey.get_position` / `.QuiverKey.set_position` +- `.QuiverKey.label` -> `.QuiverKey.get_label_text` / `.QuiverKey.set_label_text` +- `.QuiverKey.labelcolor` -> `.QuiverKey.get_label_color` / `.QuiverKey.set_label_color` +- `.QuiverKey.labelpos` -> `.QuiverKey.get_label_pos` / `.QuiverKey.set_label_pos` +- `.QuiverKey.labelsep` is now read-only as it used a different unit (pixels) + than the constructor (inches), and was automatically overwritten; + `.QuiverKey.get_labelsep` and `.QuiverKey.set_labelsep` have been added which + use inches diff --git a/doc/api/next_api_changes/deprecations/18794-ES.rst b/doc/api/next_api_changes/deprecations/18794-ES.rst new file mode 100644 index 000000000000..2137c3beb323 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/18794-ES.rst @@ -0,0 +1,10 @@ +``QuiverKey`` internal Artists +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Access to the following `.quiver.QuiverKey` internal Artists is now deprecated. +You may instead use `.quiver.QuiverKey`-level methods to modify these Artists. + +- ``QuiverKey.text`` +- ``QuiverKey.vector`` +- ``QuiverKey.verts`` + diff --git a/doc/users/next_whats_new/quiverkey.rst b/doc/users/next_whats_new/quiverkey.rst new file mode 100644 index 000000000000..43c9d9fb9231 --- /dev/null +++ b/doc/users/next_whats_new/quiverkey.rst @@ -0,0 +1,20 @@ +``QuiverKey`` properties are now modifiable +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The `.QuiverKey` object returned by `.pyplot.quiverkey` and `.axes.Axes.quiverkey` +formerly saved various properties as attributes during initialization. However, +modifying these attributes may or may not have had an effect on the final result. + +Now all such properties have getters and setters, and may be modified after creation: + +- `.QuiverKey.X` -> `.QuiverKey.get_x` / `.QuiverKey.set_x` / + `.QuiverKey.get_position` / `.QuiverKey.set_position` +- `.QuiverKey.Y` -> `.QuiverKey.get_y` / `.QuiverKey.set_y` / + `.QuiverKey.get_position` / `.QuiverKey.set_position` +- `.QuiverKey.label` -> `.QuiverKey.get_label_text` / `.QuiverKey.set_label_text` +- `.QuiverKey.labelcolor` -> `.QuiverKey.get_label_color` / `.QuiverKey.set_label_color` +- `.QuiverKey.labelpos` -> `.QuiverKey.get_label_pos` / `.QuiverKey.set_label_pos` +- `.QuiverKey.labelsep` is now read-only as it used a different unit (pixels) + than the constructor (inches), and was automatically overwritten; + `.QuiverKey.get_labelsep` and `.QuiverKey.set_labelsep` have been added which + use inches diff --git a/lib/matplotlib/quiver.py b/lib/matplotlib/quiver.py index 8fa1962d6321..cd657488eac2 100644 --- a/lib/matplotlib/quiver.py +++ b/lib/matplotlib/quiver.py @@ -238,6 +238,10 @@ class QuiverKey(martist.Artist): valign = {'N': 'bottom', 'S': 'top', 'E': 'center', 'W': 'center'} pivot = {'N': 'middle', 'S': 'middle', 'E': 'tip', 'W': 'tail'} + text = _api.deprecate_privatize_attribute('3.9', alternative='QuiverKey methods') + vector = _api.deprecate_privatize_attribute('3.9') + verts = _api.deprecate_privatize_attribute('3.9') + def __init__(self, Q, X, Y, U, label, *, angle=0, coordinates='axes', color=None, labelsep=0.1, labelpos='N', labelcolor=None, fontproperties=None, **kwargs): @@ -290,31 +294,136 @@ def __init__(self, Q, X, Y, U, label, """ super().__init__() self.Q = Q - self.X = X - self.Y = Y self.U = U self.angle = angle self.coord = coordinates self.color = color - self.label = label self._labelsep_inches = labelsep - self.labelpos = labelpos - self.labelcolor = labelcolor + _api.check_in_list(['N', 'S', 'E', 'W'], labelpos=labelpos) + self._labelpos = labelpos self.fontproperties = fontproperties or dict() self.kw = kwargs - self.text = mtext.Text( - text=label, - horizontalalignment=self.halign[self.labelpos], - verticalalignment=self.valign[self.labelpos], - fontproperties=self.fontproperties) - if self.labelcolor is not None: - self.text.set_color(self.labelcolor) + self._text = mtext.Text( + x=X, y=Y, text=label, + horizontalalignment=self.halign[self._labelpos], + verticalalignment=self.valign[self._labelpos], + fontproperties=self.fontproperties, + color=labelcolor) self._dpi_at_last_init = None self.zorder = Q.zorder + 0.1 + def get_x(self): + """Return the *x* position of the QuiverKey.""" + return self._text.get_position()[0] + + def set_x(self, x): + """ + Set the *x* position of the QuiverKey. + + Parameters + ---------- + x : float + The *x* location of the key. + """ + self._text.set_x(x) + self.stale = True + + X = property(get_x, set_x) + + def get_y(self): + """Return the *y* position of the QuiverKey.""" + return self._text.get_position()[1] + + def set_y(self, y): + """ + Set the *y* position of the QuiverKey. + + Parameters + ---------- + y : float + The *y* location of the key. + """ + self._text.set_y(y) + self.stale = True + + Y = property(get_y, set_y) + + def get_position(self): + """Return the (x, y) position of the QuiverKey.""" + return self._text.get_position() + + def set_position(self, xy): + """ + Set the position of the QuiverKey. + + Parameters + ---------- + xy : (float, float) + The (*x*, *y*) position of the QuiverKey. + """ + self._text.set_position(xy) + self.stale = True + + def get_label_text(self): + """Return the label string.""" + return self._text.get_text() + + def set_label_text(self, text): + """Set the label string.""" + self._text.set_text(text) + self.stale = True + + label = property(get_label_text, set_label_text, doc="The label string.") + + def get_label_color(self): + """Return the label color.""" + return self._text.get_color() + + def set_label_color(self, labelcolor): + """Set the label color.""" + self._text.set_color(labelcolor) + self.stale = True + + labelcolor = property(get_label_color, set_label_color, doc="The label color.") + + def get_label_pos(self): + """Return the label position.""" + return self._labelpos + + def set_label_pos(self, labelpos): + """ + Set the label position. + + Parameters + ---------- + labelpos : {'N', 'S', 'E', 'W'} + Position the label above, below, to the right, to the left of the + arrow, respectively. + """ + _api.check_in_list(['N', 'S', 'E', 'W'], labelpos=labelpos) + self._labelpos = labelpos + self._text.set_horizontalalignment(self.halign[labelpos]) + self._text.set_verticalalignment(self.valign[labelpos]) + self._update_text_transform() + self._initialized = False + self.stale = True + + labelpos = property(get_label_pos, set_label_pos, doc="The label position.") + + def get_labelsep(self): + """Return the distance between the arrow and label in inches.""" + return self._labelsep_inches + + def set_labelsep(self, labelsep): + """Set the distance between the arrow and label in inches.""" + self._labelsep_inches = labelsep + self._update_text_transform() + self.stale = True + @property def labelsep(self): + """Return the distance between the arrow and label in pixels.""" return self._labelsep_inches * self.Q.axes.figure.dpi def _init(self): @@ -322,41 +431,42 @@ def _init(self): if self.Q._dpi_at_last_init != self.Q.axes.figure.dpi: self.Q._init() self._set_transform() - with cbook._setattr_cm(self.Q, pivot=self.pivot[self.labelpos], + with cbook._setattr_cm(self.Q, pivot=self.pivot[self._labelpos], # Hack: save and restore the Umask Umask=ma.nomask): u = self.U * np.cos(np.radians(self.angle)) v = self.U * np.sin(np.radians(self.angle)) - self.verts = self.Q._make_verts([[0., 0.]], - np.array([u]), np.array([v]), 'uv') + self._verts = self.Q._make_verts( + [[0., 0.]], np.array([u]), np.array([v]), 'uv') kwargs = self.Q.polykw kwargs.update(self.kw) - self.vector = mcollections.PolyCollection( - self.verts, + self._vector = mcollections.PolyCollection( + self._verts, offsets=[(self.X, self.Y)], offset_transform=self.get_transform(), **kwargs) if self.color is not None: - self.vector.set_color(self.color) - self.vector.set_transform(self.Q.get_transform()) - self.vector.set_figure(self.get_figure()) + self._vector.set_color(self.color) + self._vector.set_transform(self.Q.get_transform()) + self._vector.set_figure(self.get_figure()) self._dpi_at_last_init = self.Q.axes.figure.dpi - def _text_shift(self): - return { - "N": (0, +self.labelsep), - "S": (0, -self.labelsep), - "E": (+self.labelsep, 0), - "W": (-self.labelsep, 0), - }[self.labelpos] + def _update_text_transform(self): + x, y = { + "N": (0, +self._labelsep_inches), + "S": (0, -self._labelsep_inches), + "E": (+self._labelsep_inches, 0), + "W": (-self._labelsep_inches, 0), + }[self._labelpos] + self._text.set_transform( + transforms.offset_copy(self.get_transform(), self.figure, x=x, y=y)) @martist.allow_rasterization def draw(self, renderer): self._init() - self.vector.draw(renderer) - pos = self.get_transform().transform((self.X, self.Y)) - self.text.set_position(pos + self._text_shift()) - self.text.draw(renderer) + self._vector.draw(renderer) + self._update_text_transform() + self._text.draw(renderer) self.stale = False def _set_transform(self): @@ -369,15 +479,14 @@ def _set_transform(self): def set_figure(self, fig): super().set_figure(fig) - self.text.set_figure(fig) + self._text.set_figure(fig) def contains(self, mouseevent): if self._different_canvas(mouseevent): return False, {} # Maybe the dictionary should allow one to # distinguish between a text hit and a vector hit. - if (self.text.contains(mouseevent)[0] or - self.vector.contains(mouseevent)[0]): + if self._text.contains(mouseevent)[0] or self._vector.contains(mouseevent)[0]: return True, {} return False, {} diff --git a/lib/matplotlib/quiver.pyi b/lib/matplotlib/quiver.pyi index 2a043a92b4b5..c4bacf470d8e 100644 --- a/lib/matplotlib/quiver.pyi +++ b/lib/matplotlib/quiver.pyi @@ -17,18 +17,15 @@ class QuiverKey(martist.Artist): valign: dict[Literal["N", "S", "E", "W"], Literal["top", "center", "bottom"]] pivot: dict[Literal["N", "S", "E", "W"], Literal["middle", "tip", "tail"]] Q: Quiver - X: float - Y: float U: float angle: float coord: Literal["axes", "figure", "data", "inches"] color: ColorType | None - label: str - labelpos: Literal["N", "S", "E", "W"] - labelcolor: ColorType | None fontproperties: dict[str, Any] kw: dict[str, Any] text: Text + vector: mcollections.PolyCollection + verts: ArrayLike zorder: float def __init__( self, @@ -47,6 +44,25 @@ class QuiverKey(martist.Artist): fontproperties: dict[str, Any] | None = ..., **kwargs ) -> None: ... + def get_x(self) -> float: ... + def set_x(self, x: float) -> None: ... + X: float + def get_y(self) -> float: ... + def set_y(self, y: float) -> None: ... + Y: float + def get_position(self) -> tuple[float, float]: ... + def set_position(self, xy: tuple[float, float]) -> None: ... + def get_label_text(self) -> str: ... + def set_label_text(self, text: str) -> None: ... + label: str + def get_label_color(self) -> ColorType | None: ... + def set_label_color(self, labelcolor: ColorType | None) -> None: ... + labelcolor: ColorType | None + def get_label_pos(self) -> Literal["N", "S", "E", "W"]: ... + def set_label_pos(self, labelpos: Literal["N", "S", "E", "W"]) -> None: ... + labelpos: Literal["N", "S", "E", "W"] + def get_labelsep(self) -> float: ... + def set_labelsep(self, labelsep: float) -> None: ... @property def labelsep(self) -> float: ... def set_figure(self, fig: Figure) -> None: ... diff --git a/lib/matplotlib/tests/test_quiver.py b/lib/matplotlib/tests/test_quiver.py index 7c5a9d343530..2b8a822d9d66 100644 --- a/lib/matplotlib/tests/test_quiver.py +++ b/lib/matplotlib/tests/test_quiver.py @@ -113,12 +113,15 @@ def test_quiver_with_key(): fig, ax = plt.subplots() ax.margins(0.1) Q = draw_quiver(ax) - ax.quiverkey(Q, 0.5, 0.95, 2, - r'$2\, \mathrm{m}\, \mathrm{s}^{-1}$', - angle=-10, - coordinates='figure', - labelpos='W', - fontproperties={'weight': 'bold', 'size': 'large'}) + qk = ax.quiverkey(Q, 0, 0, 2, '', + angle=-10, coordinates='figure', + labelpos='N', labelcolor='b', + fontproperties={'weight': 'bold', 'size': 'large'}) + qk.set_x(0.5) + qk.set_y(0.95) + qk.set_label_text(r'$2\, \mathrm{m}\, \mathrm{s}^{-1}$') + qk.set_label_pos('W') + qk.set_label_color('k') # Go back to default to keep same test image. @image_comparison(['quiver_single_test_image.png'], remove_text=True) @@ -147,8 +150,8 @@ def test_quiver_key_pivot(): ax.set_ylim(-2, 11) ax.quiverkey(q, 0.5, 1, 1, 'N', labelpos='N') ax.quiverkey(q, 1, 0.5, 1, 'E', labelpos='E') - ax.quiverkey(q, 0.5, 0, 1, 'S', labelpos='S') - ax.quiverkey(q, 0, 0.5, 1, 'W', labelpos='W') + ax.quiverkey(q, 0.5, 0, 1, 'S').set_label_pos('S') + ax.quiverkey(q, 0, 0.5, 1, 'W').set_label_pos('W') @image_comparison(['quiver_key_xy.png'], remove_text=True) @@ -264,7 +267,7 @@ def test_quiverkey_angles(): qk = ax.quiverkey(q, 1, 1, 2, 'Label') # The arrows are only created when the key is drawn fig.canvas.draw() - assert len(qk.verts) == 1 + assert len(qk._verts) == 1 def test_quiverkey_angles_xy_aitoff(): @@ -293,7 +296,7 @@ def test_quiverkey_angles_xy_aitoff(): qk = ax.quiverkey(q, 0, 0, 1, '1 units') fig.canvas.draw() - assert len(qk.verts) == 1 + assert len(qk._verts) == 1 def test_quiverkey_angles_scale_units_cartesian(): @@ -320,7 +323,7 @@ def test_quiverkey_angles_scale_units_cartesian(): qk = ax.quiverkey(q, 0, 0, 1, '1 units') fig.canvas.draw() - assert len(qk.verts) == 1 + assert len(qk._verts) == 1 def test_quiver_setuvc_numbers():