From f5b5e3485a4674c64ad6494e62ea861498068f50 Mon Sep 17 00:00:00 2001 From: Ines Cachola Date: Wed, 4 Jun 2025 14:48:01 +0100 Subject: [PATCH] =?UTF-8?q?Feat=20#29704:=20Add=20bbox=20support=20to=20qu?= =?UTF-8?q?iverkey=20It=20is=20now=20possible=20to=20add=20a=20bbox=20(bou?= =?UTF-8?q?nding=20box)=20around=20the=20entire=20quiverkey=20label=20?= =?UTF-8?q?=E2=80=94=20including=20both=20the=20arrow=20and=20the=20text?= =?UTF-8?q?=20=E2=80=94=20using=20the=20bbox=20keyword=20argument.=20Previ?= =?UTF-8?q?ously,=20bounding=20boxes=20could=20only=20be=20added=20manuall?= =?UTF-8?q?y=20or=20were=20limited=20to=20wrapping=20just=20the=20text,=20?= =?UTF-8?q?not=20the=20full=20key.=20This=20enhancement=20allows=20users?= =?UTF-8?q?=20to=20apply=20background=20color,=20border,=20and=20padding?= =?UTF-8?q?=20around=20the=20complete=20key=20for=20better=20visibility=20?= =?UTF-8?q?and=20styling.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mehak Khosa --- .../quiverkey_with_bbox.py | 45 ++ lib/matplotlib/quiver.py | 112 +++- lib/matplotlib/tests/test_quiver.py | 524 ++++++++++++++++++ 3 files changed, 676 insertions(+), 5 deletions(-) create mode 100644 galleries/examples/images_contours_and_fields/quiverkey_with_bbox.py diff --git a/galleries/examples/images_contours_and_fields/quiverkey_with_bbox.py b/galleries/examples/images_contours_and_fields/quiverkey_with_bbox.py new file mode 100644 index 000000000000..3da634ce0c88 --- /dev/null +++ b/galleries/examples/images_contours_and_fields/quiverkey_with_bbox.py @@ -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')) + +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` diff --git a/lib/matplotlib/quiver.py b/lib/matplotlib/quiver.py index 91c510ca7060..a2d3ed913e01 100644 --- a/lib/matplotlib/quiver.py +++ b/lib/matplotlib/quiver.py @@ -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 @@ -331,9 +332,9 @@ def __init__(self, Q, X, Y, U, label, 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 + FancyBboxPatch, such as *facecolor*, *edgecolor*, *linewidth*. """ - super().__init__() self.Q = Q self.X = X self.Y = Y @@ -357,7 +358,9 @@ def __init__(self, Q, X, Y, U, label, 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) + @property def labelsep(self): return self._labelsep_inches * self.Q.axes.get_figure(root=True).dpi @@ -394,13 +397,112 @@ def _text_shift(self): "E": (+self.labelsep, 0), "W": (-self.labelsep, 0), }[self.labelpos] + + 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 + + # Update vector offset transform if vector exists, to keep arrow position current. + # 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()) + + text_bbox = self.text.get_window_extent(renderer) + + # Calculate arrow (vector) bounding box by transforming its vertices with offset. + # If vector or offsets are missing, fallback to text bbox to avoid errors. + if hasattr(self, 'vector') and self.vector is not None: + + offsets_display = self.vector.get_offset_transform().transform(self.vector.get_offsets()) + + 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() + + arrow_display_coords = [] + 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], + local_transformed[1] - vector_transform.transform((0, 0))[1] + arrow_center_display[1]) + arrow_display_coords.append(display_point) + + arrow_display_coords = np.array(arrow_display_coords) + + # 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]) + + 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 + 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 + + # 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) + + + 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 + + + box_patch = patches.FancyBboxPatch( + (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) - @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) self.text.draw(renderer) self.stale = False diff --git a/lib/matplotlib/tests/test_quiver.py b/lib/matplotlib/tests/test_quiver.py index 1205487cfe94..2af88303781b 100644 --- a/lib/matplotlib/tests/test_quiver.py +++ b/lib/matplotlib/tests/test_quiver.py @@ -3,10 +3,14 @@ import numpy as np import pytest +import cartopy.crs as ccrs + from matplotlib import pyplot as plt from matplotlib.testing.decorators import image_comparison from matplotlib.testing.decorators import check_figures_equal +from matplotlib import patches +from matplotlib import colors as colors def draw_quiver(ax, **kwargs): @@ -385,3 +389,523 @@ def draw_quiverkey_setzorder(fig, zorder=None): def test_quiverkey_zorder(fig_test, fig_ref, zorder): draw_quiverkey_zorder_argument(fig_test, zorder=zorder) draw_quiverkey_setzorder(fig_ref, zorder=zorder) + +#Tests for Quiverkey and text inside bbox. +def test_quiverkey_bbox_basic(): + """ + Test that a custom bbox passed to quiverkey is properly applied. + + This test verifies: + - That the quiverkey generates a 'bbox_patch' attribute. + - That the facecolor of the drawn FancyBboxPatch matches the specified color. + """ + fig = plt.figure(figsize=(10, 8)) + ax = plt.axes(projection=ccrs.PlateCarree()) + ax.coastlines() + ax.set_global() + + q = ax.quiver([0], [0], [1], [1], transform=ccrs.PlateCarree()) + qk = ax.quiverkey(q, X=0.85, Y=0.95, U=1, label='1 unit', labelpos='E', + bbox=dict(facecolor='lightblue', edgecolor='blue', boxstyle='round,pad=0.3')) + + assert hasattr(qk, '_bbox') and qk._bbox is not None + assert qk._bbox['facecolor'] == 'lightblue' + assert qk._bbox['edgecolor'] == 'blue' + +def test_quiverkey_without_bbox(): + """ + Test that a quiverkey created without a bbox still works correctly. + + This test verifies: + - That no error is raised when no bbox is specified. + - That the label text of the quiverkey is correctly set. + """ + fig, ax = plt.subplots(figsize=(8, 6)) + q = ax.quiver([0], [0], [3], [1], color='black', scale=30) + qk = ax.quiverkey(q, X=0.85, Y=0.15, U=3, label='3 units', labelpos='N') + + assert qk.label == '3 units' + +def test_quiverkey_all_labelpos(): + """ + Test that quiverkeys correctly support all label positions ('N', 'S', 'E', 'W') + by placing the label on the opposite side to avoid overlap. + + This test: + - Creates four axes with quiverkeys positioned roughly at N, S, E, W. + - Sets the label position opposite to the quiverkey position for visibility. + - Adds a bbox for visual clarity. + - Asserts that the label position is set correctly. + """ + fig, axes = plt.subplots(2, 2, figsize=(12, 10), subplot_kw={'projection': ccrs.PlateCarree()}) + axes = axes.flatten() + positions = ['N', 'S', 'E', 'W'] + + for ax, pos in zip(axes, positions): + ax.coastlines() + ax.set_global() + q = ax.quiver([0], [0], [1], [0.5], transform=ccrs.PlateCarree()) + + if pos == 'N': + X, Y, labelpos = 0.5, 0.7, 'S' + elif pos == 'S': + X, Y, labelpos = 0.5, 0.25, 'N' + elif pos == 'E': + X, Y, labelpos = 0.75, 0.5, 'W' + else: # 'W' + X, Y, labelpos = 0.25, 0.5, 'E' + + qk = ax.quiverkey(q, X=X, Y=Y, U=1, + label=f'{pos} position', + labelpos=labelpos, + bbox=dict(facecolor='red', alpha=0.3, edgecolor='black', linewidth=1)) + + assert qk.labelpos == labelpos + + plt.close(fig) + +def test_quiverkey_different_angles(): + """ + Test that quiverkeys correctly apply different arrow angles. + + This test: + - Creates a single plot with one quiver. + - Adds four quiverkeys with angles: 0°, 45°, 90°, and 135°. + - Each quiverkey is positioned to be fully visible within the plot. + - Asserts that each quiverkey stores the correct angle. + """ + fig, ax = plt.subplots(figsize=(8, 6)) + + q = ax.quiver([0], [1], [1], [0], angles='xy', scale_units='xy', scale=1) + + angles = [0, 45, 90, 135] + positions = [ + (0.3, 0.85), + (0.7, 0.85), + (0.3, 0.15), + (0.7, 0.15), + ] + + for angle, (xk, yk) in zip(angles, positions): + qk = ax.quiverkey(q, X=xk, Y=yk, U=0.2, angle=angle, + label=f'{angle}° arrow', labelpos='E', + bbox=dict(facecolor='yellow', alpha=0.5, edgecolor='red'), + coordinates='axes') + + assert qk.angle == angle + + ax.set_xlim(0, 2) + ax.set_ylim(0, 2) + ax.set_aspect('equal') + plt.title("Quiverkeys fully inside axes with different angles") + +def test_quiverkey_various_locations(): + """ + Test that quiverkeys can be created with different label texts and positions. + + This test: + - Creates a global vector field using regularly spaced coordinates. + - Adds multiple quiverkeys with varying labels at specified normalized positions. + - Uses an empty `bbox` to ensure label functionality sem dependência de estilo visual. + + Note: + - This test currently does not assert label correctness or positions. + """ + fig = plt.figure(figsize=(12, 8)) + ax = plt.axes(projection=ccrs.PlateCarree()) + ax.coastlines() + ax.set_global() + + lons = np.linspace(-180, 180, 10) + lats = np.linspace(-60, 60, 8) + u = np.random.random((len(lats), len(lons))) - 0.5 + v = np.random.random((len(lats), len(lons))) - 0.5 + q = ax.quiver(lons, lats, u, v, transform=ccrs.PlateCarree()) + + locations = ['Top Left', 'Top Right', 'Bottom Left', 'Bottom Right', 'Bottom Center'] + positions = [ + (0.1, 0.9), # Top Left + (0.9, 0.9), # Top Right + (0.1, 0.1), # Bottom Left + (0.9, 0.1), # Bottom Right + (0.5, 0.1) # Bottom Center + ] + + for label, (x, y) in zip(locations, positions): + qk = ax.quiverkey(q, X=x, Y=y, U=0.5, label=label, labelpos='E', bbox={}) + + assert qk.label == label + + plt.close(fig) + +def test_quiverkey_boxstyles(): + """ + Test that quiverkeys correctly apply various box styles to their surrounding bbox_patch. + + This test: + - Defines a list of different `boxstyle` configurations for the bbox. + - Places each quiverkey in a different vertical position to avoid overlap. + - Asserts that the label is correct and that the bbox was created. + + """ + fig, ax = plt.subplots(figsize=(12, 10)) + q = ax.quiver([-0.5], [0], [1], [1]) + ax.set_xlim(-2, 2) + ax.set_ylim(-2, 2) + + boxstyles = [ + ('square', 'Square'), + ('round,pad=1.2', 'Round\nLarge Pad'), + ('round4,pad=1.2', 'Round4\nLarge Pad'), + ('sawtooth,pad=1.2', 'Sawtooth\nLarge Pad'), + ('roundtooth,pad=1.2', 'Roundtooth\nLarge Pad'), + ('round,pad=1.5', 'Round\nBig Pad'), + ('round,pad=1.0,rounding_size=0.6', 'Custom\nRound') + ] + + for i, (style, label) in enumerate(boxstyles): + y_pos = 0.9 - i * 0.12 + qk = ax.quiverkey(q, X=0.1, Y=y_pos, U=0.5, label=label, labelpos='E', + labelcolor='black', + bbox=dict(boxstyle=style, facecolor='lightyellow', edgecolor='black', linewidth=2)) + + + assert qk.label == label + assert qk._bbox is not None + assert qk._bbox['facecolor'] == 'lightyellow' + assert qk._bbox['edgecolor'] == 'black' + + plt.close(fig) + +def test_quiverkey_color_matching(): + """ + Test that the quiverkey label correctly inherits the specified color. + + This test: + - Draws a single vector in a specific color (`darkgreen`) using `quiver`. + - Adds a quiverkey with the `labelcolor` explicitly set to match the vector color. + - Asserts that the label of the quiverkey adopts the intended color, + ensuring visual consistency between the vector and its legend. + """ + fig = plt.figure(figsize=(10, 6)) + ax = plt.axes(projection=ccrs.PlateCarree()) + ax.set_global() + ax.coastlines() + + vector_color = 'darkgreen' + q = ax.quiver(0, 0, 1, 0, color=vector_color, transform=ccrs.PlateCarree()) + + qk = ax.quiverkey(q, X=0.5, Y=0.9, U=1, + label='1 unit (match color)', labelpos='E', + labelcolor=vector_color, + bbox=dict(boxstyle='round,pad=0.5', facecolor='white', edgecolor='black')) + + assert qk.text.get_color() == vector_color + plt.close(fig) + +def test_quiverkey_without_bbox_creates_no_fancybox(): + """ + Test that no FancyBboxPatch is created when `bbox=None` is passed. + + This test: + - Creates a simple quiver plot with a single vector. + - Adds a quiverkey with `bbox=None`, meaning no background box is requested. + - Asserts that no `FancyBboxPatch` is present among the children of the quiverkey, + verifying that the label appears without a styled box. + """ + + fig, ax = plt.subplots() + q = ax.quiver([0], [0], [1], [0]) + qk = ax.quiverkey(q, X=0.5, Y=0.5, U=1, label="test", bbox=None) + + has_fancybox = any(isinstance(child, patches.FancyBboxPatch) + for child in qk.get_children()) + assert not has_fancybox + plt.close(fig) + +def test_quiverkey_position_stable_on_resize_with_bbox(): + """ + Test that the position of the quiverkey label remains stable in axes coordinates + after resizing the figure, even when a bbox is applied. + + This test: + - Creates a quiver plot with a single vector. + - Adds a quiverkey label at a specified position with a visible `bbox` (styled background box). + - Forces a draw to render all elements. + - Records the quiverkey text position in axes coordinates before resizing. + - Resizes the figure and redraws it. + - Records the position again after resizing. + - Asserts that the axes-relative position of the label remains stable, + verifying that applying a `bbox` does not affect layout stability. + """ + fig, ax = plt.subplots() + q = ax.quiver([0], [0], [1], [0]) + + qk = ax.quiverkey( + q, X=0.3, Y=0.6, U=1, label='resize test with bbox', + bbox=dict(boxstyle='round,pad=0.2', facecolor='lightblue', edgecolor='black') + ) + + fig.canvas.draw() + + display_pos_before = qk.text.get_window_extent().get_points().mean(axis=0) + axes_pos_before = ax.transAxes.inverted().transform(display_pos_before) + + fig.set_size_inches(10, 8) + fig.canvas.draw() + + display_pos_after = qk.text.get_window_extent().get_points().mean(axis=0) + axes_pos_after = ax.transAxes.inverted().transform(display_pos_after) + + assert np.allclose(axes_pos_before, axes_pos_after, atol=0.01), ( + f"Axes position changed with bbox: before {axes_pos_before}, after {axes_pos_after}" + ) + + plt.close(fig) + +def find_bbox_patch(qk): + " Aulixiar Function - Searches for and returns the FancyBboxPatch child of the quiverkey qk, or None if it does not exist." + for child in qk.get_children(): + if isinstance(child, patches.FancyBboxPatch): + return child + return None + +def test_quiverkey_bbox_default_properties(): + """ + Test that no FancyBboxPatch is created when `bbox=None` is passed. + + This test: + - Creates a quiver plot with multiple vectors. + - Adds a quiverkey with `bbox=None`, meaning no background box is requested. + - Asserts that the internal `_bbox` attribute is None. + - Asserts that no `bbox_patch` exists, confirming that no `FancyBboxPatch` was created + and that the label appears without a styled box. + """ + fig, ax = plt.subplots(figsize=(8, 6)) + + q = ax.quiver([0, 40, -60], [0, 20, -30], [3, -2, 1], [1, 2, -3], + color='black', scale=30) + qk = ax.quiverkey(q, X=0.85, Y=0.15, U=3, label='3 units', labelpos='N', bbox = None) + + assert qk._bbox is None, "QuiverKey should have no bbox by default" + assert not hasattr(qk, 'bbox_patch') or qk.bbox_patch is None, \ + "No bbox_patch should exist when bbox=None" + + plt.close(fig) + +def test_quiverkey_bbox_style_update(): + """ + Test that QuiverKey bbox properties can be set and updated. + + This test: + - Creates a quiver plot with a few vectors. + - Adds a quiverkey with an initial `bbox` specifying facecolor, edgecolor, boxstyle, and alpha. + - Asserts that the bbox properties are stored correctly. + - Ensures that rendering with the initial bbox completes without errors. + - Updates the bbox with new styling parameters. + - Verifies that the new bbox properties are applied. + - Ensures that rendering with the updated bbox also completes without errors. + """ + fig, ax = plt.subplots(figsize=(8, 6)) + + q = ax.quiver([0, 40, -60], [0, 20, -30], [3, -2, 1], [1, 2, -3], + color='black', scale=30) + + bbox_props = dict(facecolor='lightblue', edgecolor='blue', + boxstyle='round,pad=0.3', alpha=0.8) + qk = ax.quiverkey(q, X=0.85, Y=0.15, U=3, label='3 units', labelpos='N', + bbox=bbox_props) + + assert qk._bbox is not None, "QuiverKey should have bbox when specified" + assert qk._bbox['facecolor'] == 'lightblue', "Facecolor should be lightblue" + assert qk._bbox['edgecolor'] == 'blue', "Edgecolor should be blue" + assert qk._bbox['boxstyle'] == 'round,pad=0.3', "Boxstyle should be round,pad=0.3" + assert qk._bbox['alpha'] == 0.8, "Alpha should be 0.8" + + try: + fig.canvas.draw() + rendering_success = True + except Exception as e: + rendering_success = False + + assert rendering_success, "Rendering with bbox should work without errors" + + new_bbox = dict(facecolor='red', edgecolor='black', boxstyle='square,pad=0.5') + qk._bbox.update(new_bbox) + + assert qk._bbox['facecolor'] == 'red', "Facecolor should be updated to red" + assert qk._bbox['edgecolor'] == 'black', "Edgecolor should be updated to black" + assert qk._bbox['boxstyle'] == 'square,pad=0.5', "Boxstyle should be updated to square" + + try: + fig.canvas.draw() + updated_rendering_success = True + except Exception as e: + updated_rendering_success = False + + assert updated_rendering_success, "Rendering with updated bbox should work" + + plt.close(fig) + +def test_quiverkey_bbox_with_different_coordinate_systems(): + """ + Test that bbox functionality works correctly across different coordinate systems. + + This test: + - Creates quiverkeys using 'axes', 'figure', and 'data' coordinate systems. + - Verifies that bbox is created successfully for each coordinate system. + - Ensures coordinate system is correctly set on the quiverkey object. + """ + coordinate_systems = ['axes', 'figure', 'data'] + + for coord_sys in coordinate_systems: + fig, ax = plt.subplots(figsize=(8, 6)) + q = ax.quiver([0], [0], [1], [1], color='blue') + + if coord_sys == 'axes': + X, Y = 0.8, 0.8 + elif coord_sys == 'figure': + X, Y = 0.8, 0.8 + elif coord_sys == 'data': + X, Y = 0.5, 0.5 + ax.set_xlim(-1, 1) + ax.set_ylim(-1, 1) + + qk = ax.quiverkey(q, X=X, Y=Y, U=1, + label=f'{coord_sys} coords', + coordinates=coord_sys, + bbox=dict(facecolor='lightgreen', edgecolor='darkgreen')) + + assert qk.coord == coord_sys, f"Coordinate system should be {coord_sys}" + assert qk._bbox is not None, f"BBox should exist for {coord_sys} coordinates" + + fig.canvas.draw() + + plt.close(fig) + +def test_quiverkey_bbox_edge_cases(): + """ + Test that bbox handles edge cases with various label types and extreme positions. + + This test: + - Creates quiverkeys with long, short, and multiline labels. + - Tests positioning at figure boundaries. + - Verifies that bbox is created successfully for all edge cases. + """ + fig, ax = plt.subplots(figsize=(12, 8)) + q = ax.quiver([0], [0], [1], [1]) + + # Test with very long label + long_label = "This is a very long label that might cause bbox issues" + qk_long = ax.quiverkey(q, X=0.1, Y=0.9, U=1, label=long_label, + bbox=dict(facecolor='cyan', edgecolor='blue')) + + # Test with very short label + qk_short = ax.quiverkey(q, X=0.9, Y=0.9, U=1, label="X", + bbox=dict(facecolor='pink', edgecolor='red')) + + # Test with multiline label + multiline_label = "Line 1\nLine 2\nLine 3" + qk_multi = ax.quiverkey(q, X=0.5, Y=0.1, U=1, label=multiline_label, + bbox=dict(facecolor='lightblue', edgecolor='navy')) + + # Test positioning at figure edges + qk_edge = ax.quiverkey(q, X=0.01, Y=0.01, U=1, label="Edge case", + bbox=dict(facecolor='yellow', edgecolor='orange')) + + # Verify all have bbox + for qk in [qk_long, qk_short, qk_multi, qk_edge]: + assert qk._bbox is not None, "All quiverkeys should have bbox" + + fig.canvas.draw() + plt.close(fig) + +def test_quiverkey_bbox_with_transparent_elements(): + """ + Test bbox behavior with transparent and semi-transparent styling. + + This test: + - Creates quiverkeys with fully transparent, semi-transparent, and opaque bbox elements. + - Tests transparent text rendering with bbox backgrounds. + - Verifies that bbox objects are created regardless of transparency settings. + """ + fig, ax = plt.subplots(figsize=(8, 6)) + q = ax.quiver([0], [0], [1], [1], alpha=0.5) + + # Test with fully transparent bbox + qk_transparent = ax.quiverkey(q, X=0.3, Y=0.7, U=1, label="Transparent", + bbox=dict(facecolor='red', alpha=0.0, edgecolor='black')) + + # Test with semi-transparent bbox + qk_semi = ax.quiverkey(q, X=0.7, Y=0.7, U=1, label="Semi-transparent", + bbox=dict(facecolor='blue', alpha=0.5, edgecolor='navy')) + + # Test with transparent text + qk_text_alpha = ax.quiverkey(q, X=0.5, Y=0.3, U=1, label="Text Alpha", + labelcolor=(0, 0, 0, 0.5), + bbox=dict(facecolor='green', alpha=0.8)) + + for qk in [qk_transparent, qk_semi, qk_text_alpha]: + assert qk._bbox is not None, "All quiverkeys should have bbox" + + fig.canvas.draw() + plt.close(fig) + +def test_quiverkey_bbox_zorder_interactions(): + """ + Test that bbox respects zorder settings and doesn't interfere with other elements. + + This test: + - Creates overlapping elements: a background rectangle and a quiverkey with bbox. + - Uses different zorder values to test proper layering behavior. + - Verifies that the quiverkey's zorder is correctly set and bbox is created. + """ + fig, ax = plt.subplots(figsize=(8, 6)) + q = ax.quiver([0], [0], [1], [1], zorder=5) + + ax.add_patch(patches.Rectangle((0.4, 0.4), 0.2, 0.2, + facecolor='red', alpha=0.7, zorder=1, + transform=ax.transData)) + + qk = ax.quiverkey(q, X=0.5, Y=0.5, U=1, label="ZOrder Test", + coordinates='data', + zorder=10, + bbox=dict(facecolor='white', edgecolor='black', alpha=0.8)) + + assert qk.zorder == 10, "QuiverKey should have zorder 10" + assert qk._bbox is not None, "BBox should exist" + + fig.canvas.draw() + plt.close(fig) + +def test_quiverkey_bbox_error_handling(): + """ + Test error handling for invalid bbox parameters. + + This test: + - Tests bbox creation with invalid parameters (like invalid colors). + - Verifies graceful error handling or successful recovery from bad inputs. + - Tests bbox creation with empty parameter dictionaries. + - Ensures robust behavior when users provide problematic bbox configurations. + """ + fig, ax = plt.subplots(figsize=(8, 6)) + q = ax.quiver([0], [0], [1], [1]) + + # Test with invalid bbox parameter (should not crash) + try: + qk = ax.quiverkey(q, X=0.5, Y=0.5, U=1, label="Error Test", + bbox=dict(facecolor='invalid_color')) + + fig.canvas.draw() + test_passed = True + except Exception as e: + # If it fails, ensure it's a reasonable error + test_passed = isinstance(e, (ValueError, TypeError)) + + assert test_passed, "Should handle invalid bbox parameters gracefully" + + qk_empty = ax.quiverkey(q, X=0.3, Y=0.3, U=1, label="Empty BBox", + bbox={}) + assert qk_empty._bbox is not None, "Empty bbox dict should still create bbox" + + plt.close(fig) \ No newline at end of file