Skip to content

Add improved modification of QuiverKey properties #18794

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions doc/api/next_api_changes/behavior/18794-ES.rst
Original file line number Diff line number Diff line change
@@ -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
10 changes: 10 additions & 0 deletions doc/api/next_api_changes/deprecations/18794-ES.rst
Original file line number Diff line number Diff line change
@@ -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``
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps someone may want e.g. to set usetex or math_fontfamily or whatnot on the Text; do you really want to deprecate access to it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping text public is the pragmatic thing to do.


20 changes: 20 additions & 0 deletions doc/users/next_whats_new/quiverkey.rst
Original file line number Diff line number Diff line change
@@ -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
177 changes: 143 additions & 34 deletions lib/matplotlib/quiver.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,10 @@
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):
Expand Down Expand Up @@ -290,73 +294,179 @@
"""
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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we can do without {get,set}_{x,y} and just stick to get_position (as for Text)? the backcompat X and Y would be kept for backcompat but can use straight lambdas (X = property(lambda self: self._text.get_position()[0], self.text.set_x) -- setting stale shouldn't really matter here as Text will propagate staleness to the parent Axes anyways)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree. We only have get_x on Rectangle and FancyBboxPatch. Let's keep the API size down and only provide get/set_position.

"""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()

Check warning on line 354 in lib/matplotlib/quiver.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/quiver.py#L354

Added line #L354 was not covered by tests

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

Check warning on line 366 in lib/matplotlib/quiver.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/quiver.py#L365-L366

Added lines #L365 - L366 were not covered by tests

def get_label_text(self):
"""Return the label string."""
return self._text.get_text()

Check warning on line 370 in lib/matplotlib/quiver.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/quiver.py#L370

Added line #L370 was not covered by tests

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()

Check warning on line 381 in lib/matplotlib/quiver.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/quiver.py#L381

Added line #L381 was not covered by tests

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

Check warning on line 392 in lib/matplotlib/quiver.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/quiver.py#L392

Added line #L392 was not covered by tests

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

Check warning on line 416 in lib/matplotlib/quiver.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/quiver.py#L416

Added line #L416 was not covered by tests

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

Check warning on line 422 in lib/matplotlib/quiver.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/quiver.py#L420-L422

Added lines #L420 - L422 were not covered by tests

@property
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider deprecating this, given the potential source of confusion?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree.

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):
if True: # self._dpi_at_last_init != self.axes.figure.dpi
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):
Expand All @@ -369,15 +479,14 @@

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, {}

Expand Down
26 changes: 21 additions & 5 deletions lib/matplotlib/quiver.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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: ...
Expand Down
25 changes: 14 additions & 11 deletions lib/matplotlib/tests/test_quiver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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():
Expand All @@ -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():
Expand Down
Loading