Skip to content

Add bbox support to quiverkey #30148

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""
=========================================
Using a quiver key with bbox on a map
=========================================

This example demonstrates how to use the `bbox` argument in
`.Axes.quiverkey` to add a background box to the key label.

The vector is plotted on a global map using `cartopy`, and the key is shown
in normalized axes coordinates (`coordinates='axes'`). The background box
enhances readability, especially over detailed geographic backgrounds.

A rounded, padded white box is used for the quiver key to ensure it stands out
on the map.
"""

import matplotlib.pyplot as plt
import cartopy.crs as ccrs

fig = plt.figure(figsize=(12, 6))

# Create axes with a map projection
ax = plt.axes(projection=ccrs.PlateCarree())
ax.set_global()
ax.coastlines()

# Sample vector (in geographic coordinates)
q = ax.quiver([0], [0], [1], [1], transform=ccrs.PlateCarree())

# Add a quiver key with a visible bbox inside the map area
qk = ax.quiverkey(q, X=0.85, Y=0.1, U=1, label='1 unit',
labelpos='E',
coordinates='axes', # normalized axes coordinates (0–1)
bbox=dict(facecolor='white', edgecolor='black', boxstyle='round,pad=0.3'))

Check failure on line 34 in galleries/examples/images_contours_and_fields/quiverkey_with_bbox.py

View workflow job for this annotation

GitHub Actions / ruff

[rdjson] reported by reviewdog 🐶 Line too long (92 > 88) Raw Output: message:"Line too long (92 > 88)" location:{path:"/home/runner/work/matplotlib/matplotlib/galleries/examples/images_contours_and_fields/quiverkey_with_bbox.py" range:{start:{line:34 column:89} end:{line:34 column:93}}} source:{name:"ruff" url:"https://docs.astral.sh/ruff"} code:{value:"E501" url:"https://docs.astral.sh/ruff/rules/line-too-long"}

plt.show()
# %%
#
# .. admonition:: References
#
# The use of the following functions, methods, classes and modules is shown
# in this example:
#
# - `matplotlib.axes.Axes.quiverkey` / `matplotlib.pyplot.quiverkey`
# - `matplotlib.patches.FancyBboxPatch`
112 changes: 107 additions & 5 deletions lib/matplotlib/quiver.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import matplotlib.artist as martist
import matplotlib.collections as mcollections
from matplotlib.patches import CirclePolygon
import matplotlib.patches as patches
import matplotlib.text as mtext
import matplotlib.transforms as transforms

Expand Down Expand Up @@ -331,9 +332,9 @@
The zorder of the key. The default is 0.1 above *Q*.
**kwargs
Any additional keyword arguments are used to override vector
properties taken from *Q*.
properties taken from *Q* - right now this is only arguments from

Check failure on line 335 in lib/matplotlib/quiver.py

View workflow job for this annotation

GitHub Actions / ruff

[rdjson] reported by reviewdog 🐶 Trailing whitespace Raw Output: message:"Trailing whitespace" location:{path:"/home/runner/work/matplotlib/matplotlib/lib/matplotlib/quiver.py" range:{start:{line:335 column:78} end:{line:335 column:79}}} source:{name:"ruff" url:"https://docs.astral.sh/ruff"} code:{value:"W291" url:"https://docs.astral.sh/ruff/rules/trailing-whitespace"} suggestions:{range:{start:{line:335 column:78} end:{line:335 column:79}}}
FancyBboxPatch, such as *facecolor*, *edgecolor*, *linewidth*.
"""
super().__init__()
self.Q = Q
self.X = X
self.Y = Y
Expand All @@ -357,7 +358,9 @@
self.text.set_color(self.labelcolor)
self._dpi_at_last_init = None
self.zorder = zorder if zorder is not None else Q.zorder + 0.1

self._bbox = kwargs.pop('bbox', None)
super().__init__(**kwargs)

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

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/quiver.py#L361-L362

Added lines #L361 - L362 were not covered by tests

Check failure on line 363 in lib/matplotlib/quiver.py

View workflow job for this annotation

GitHub Actions / ruff

[rdjson] reported by reviewdog 🐶 Blank line contains whitespace Raw Output: message:"Blank line contains whitespace" location:{path:"/home/runner/work/matplotlib/matplotlib/lib/matplotlib/quiver.py" range:{start:{line:363 column:1} end:{line:363 column:7}}} source:{name:"ruff" url:"https://docs.astral.sh/ruff"} code:{value:"W293" url:"https://docs.astral.sh/ruff/rules/blank-line-with-whitespace"} suggestions:{range:{start:{line:363 column:1} end:{line:363 column:7}}}
@property
def labelsep(self):
return self._labelsep_inches * self.Q.axes.get_figure(root=True).dpi
Expand Down Expand Up @@ -394,13 +397,112 @@
"E": (+self.labelsep, 0),
"W": (-self.labelsep, 0),
}[self.labelpos]

Check failure on line 400 in lib/matplotlib/quiver.py

View workflow job for this annotation

GitHub Actions / ruff

[rdjson] reported by reviewdog 🐶 Blank line contains whitespace Raw Output: message:"Blank line contains whitespace" location:{path:"/home/runner/work/matplotlib/matplotlib/lib/matplotlib/quiver.py" range:{start:{line:400 column:1} end:{line:400 column:5}}} source:{name:"ruff" url:"https://docs.astral.sh/ruff"} code:{value:"W293" url:"https://docs.astral.sh/ruff/rules/blank-line-with-whitespace"} suggestions:{range:{start:{line:400 column:1} end:{line:400 column:5}}}
def _draw_bbox(self, renderer):
"""
Draw a bounding box around the text and vector elements of a quiver key.

This function is used to draw a rectangular or styled bounding box that
encloses both the text label and the arrow (vector) in a quiver key,
enhancing visibility and grouping them visually.

Parameters
----------
renderer : RendererBase
The renderer instance used to draw the box.
text : matplotlib.text.Text
The text label of the quiver key.
vector : matplotlib.collections.PolyCollection
The arrow graphic of the quiver key.
bbox_props : dict
A dictionary of properties passed to the FancyBboxPatch, such as
boxstyle, facecolor, edgecolor, linewidth, etc.


Notes
-----
This function transforms and aligns the vector and text elements in display
coordinates, computes a bounding rectangle around them with padding, and
draws a styled box using FancyBboxPatch.
"""
if self._bbox is None:
return

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

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/quiver.py#L429

Added line #L429 was not covered by tests

# Update vector offset transform if vector exists, to keep arrow position current.

Check failure on line 431 in lib/matplotlib/quiver.py

View workflow job for this annotation

GitHub Actions / ruff

[rdjson] reported by reviewdog 🐶 Line too long (90 > 88) Raw Output: message:"Line too long (90 > 88)" location:{path:"/home/runner/work/matplotlib/matplotlib/lib/matplotlib/quiver.py" range:{start:{line:431 column:89} end:{line:431 column:91}}} source:{name:"ruff" url:"https://docs.astral.sh/ruff"} code:{value:"E501" url:"https://docs.astral.sh/ruff/rules/line-too-long"}
# Then get the text bounding box in display coordinates for positioning.
if hasattr(self, 'vector') and self.vector is not None:
self.vector.set_offset_transform(self.get_transform())

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

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/quiver.py#L434

Added line #L434 was not covered by tests

text_bbox = self.text.get_window_extent(renderer)

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

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/quiver.py#L436

Added line #L436 was not covered by tests

# Calculate arrow (vector) bounding box by transforming its vertices with offset.

Check failure on line 438 in lib/matplotlib/quiver.py

View workflow job for this annotation

GitHub Actions / ruff

[rdjson] reported by reviewdog 🐶 Line too long (89 > 88) Raw Output: message:"Line too long (89 > 88)" location:{path:"/home/runner/work/matplotlib/matplotlib/lib/matplotlib/quiver.py" range:{start:{line:438 column:89} end:{line:438 column:90}}} source:{name:"ruff" url:"https://docs.astral.sh/ruff"} code:{value:"E501" url:"https://docs.astral.sh/ruff/rules/line-too-long"}
# If vector or offsets are missing, fallback to text bbox to avoid errors.
if hasattr(self, 'vector') and self.vector is not None:

Check failure on line 441 in lib/matplotlib/quiver.py

View workflow job for this annotation

GitHub Actions / ruff

[rdjson] reported by reviewdog 🐶 Blank line contains whitespace Raw Output: message:"Blank line contains whitespace" location:{path:"/home/runner/work/matplotlib/matplotlib/lib/matplotlib/quiver.py" range:{start:{line:441 column:1} end:{line:441 column:13}}} source:{name:"ruff" url:"https://docs.astral.sh/ruff"} code:{value:"W293" url:"https://docs.astral.sh/ruff/rules/blank-line-with-whitespace"} suggestions:{range:{start:{line:441 column:1} end:{line:441 column:13}}}
offsets_display = self.vector.get_offset_transform().transform(self.vector.get_offsets())

Check failure on line 442 in lib/matplotlib/quiver.py

View workflow job for this annotation

GitHub Actions / ruff

[rdjson] reported by reviewdog 🐶 Line too long (101 > 88) Raw Output: message:"Line too long (101 > 88)" location:{path:"/home/runner/work/matplotlib/matplotlib/lib/matplotlib/quiver.py" range:{start:{line:442 column:89} end:{line:442 column:102}}} source:{name:"ruff" url:"https://docs.astral.sh/ruff"} code:{value:"E501" url:"https://docs.astral.sh/ruff/rules/line-too-long"}

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

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/quiver.py#L442

Added line #L442 was not covered by tests

if len(offsets_display) > 0:

arrow_center_display = offsets_display[0]
arrow_verts = self.vector.get_paths()[0].vertices
vector_transform = self.vector.get_transform()

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

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/quiver.py#L446-L448

Added lines #L446 - L448 were not covered by tests

arrow_display_coords = []

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

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/quiver.py#L450

Added line #L450 was not covered by tests
for x, y in arrow_verts:

local_transformed = vector_transform.transform((x, y))
display_point = (local_transformed[0] - vector_transform.transform((0, 0))[0] + arrow_center_display[0],

Check failure on line 454 in lib/matplotlib/quiver.py

View workflow job for this annotation

GitHub Actions / ruff

[rdjson] reported by reviewdog 🐶 Line too long (124 > 88) Raw Output: message:"Line too long (124 > 88)" location:{path:"/home/runner/work/matplotlib/matplotlib/lib/matplotlib/quiver.py" range:{start:{line:454 column:89} end:{line:454 column:125}}} source:{name:"ruff" url:"https://docs.astral.sh/ruff"} code:{value:"E501" url:"https://docs.astral.sh/ruff/rules/line-too-long"}

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

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/quiver.py#L453-L454

Added lines #L453 - L454 were not covered by tests
local_transformed[1] - vector_transform.transform((0, 0))[1] + arrow_center_display[1])
arrow_display_coords.append(display_point)

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

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/quiver.py#L456

Added line #L456 was not covered by tests

arrow_display_coords = np.array(arrow_display_coords)

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

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/quiver.py#L458

Added line #L458 was not covered by tests

# Get arrow bounds in display coordinates
arrow_min_x = np.min(arrow_display_coords[:, 0])
arrow_max_x = np.max(arrow_display_coords[:, 0])
arrow_min_y = np.min(arrow_display_coords[:, 1])
arrow_max_y = np.max(arrow_display_coords[:, 1])

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

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/quiver.py#L461-L464

Added lines #L461 - L464 were not covered by tests

else:
# Fallback if we cannot get offsets or they are empty
arrow_min_x = arrow_max_x = text_bbox.x0
arrow_min_y = arrow_max_y = text_bbox.y0

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

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/quiver.py#L468-L469

Added lines #L468 - L469 were not covered by tests
else:
# Fallback if vector is not available
arrow_min_x = arrow_max_x = text_bbox.x0
arrow_min_y = arrow_max_y = text_bbox.y0

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

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/quiver.py#L472-L473

Added lines #L472 - L473 were not covered by tests

# Combine text and arrow bboxes, add padding, then draw a FancyBboxPatch around all.
min_x = min(text_bbox.x0, arrow_min_x)
min_y = min(text_bbox.y0, arrow_min_y)
max_x = max(text_bbox.x1, arrow_max_x)
max_y = max(text_bbox.y1, arrow_max_y)

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

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/quiver.py#L476-L479

Added lines #L476 - L479 were not covered by tests


padding = renderer.points_to_pixels(2)
bbox_x = min_x - padding
bbox_y = min_y - padding
bbox_width = (max_x - min_x) + 2 * padding
bbox_height = (max_y - min_y) + 2 * padding

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

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/quiver.py#L482-L486

Added lines #L482 - L486 were not covered by tests


box_patch = patches.FancyBboxPatch(

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

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/quiver.py#L489

Added line #L489 was not covered by tests
(bbox_x, bbox_y), bbox_width, bbox_height,
transform=None,
zorder=self.zorder - 0.5,
mutation_scale= 10,
**self._bbox
)
box_patch.set_clip_on(False)
box_patch.draw(renderer)

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

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/quiver.py#L496-L497

Added lines #L496 - L497 were not covered by tests

@martist.allow_rasterization
def draw(self, renderer):
# Draw bbox first (behind everything)
self._init()
self.vector.draw(renderer)
pos = self.get_transform().transform((self.X, self.Y))
self.text.set_position(pos + self._text_shift())
self._draw_bbox(renderer)
self.vector.draw(renderer)

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

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/quiver.py#L504-L505

Added lines #L504 - L505 were not covered by tests
self.text.draw(renderer)
self.stale = False

Expand Down
Loading
Loading