From c58e81a90d756d489d739a8b9d615b930816ad2a Mon Sep 17 00:00:00 2001 From: hannah Date: Sun, 22 Dec 2024 18:39:29 -0500 Subject: [PATCH] breaks up annotationbbox demo, adds annotationbbox to annotation guide, and documents the OffsetImage class Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Co-authored-by: Elliott Sales de Andrade --- .../demo_annotation_box.py | 176 +++++++++++------- galleries/users_explain/text/annotations.py | 44 +++++ lib/matplotlib/image.py | 4 +- lib/matplotlib/offsetbox.py | 64 ++++++- 4 files changed, 220 insertions(+), 68 deletions(-) diff --git a/galleries/examples/text_labels_and_annotations/demo_annotation_box.py b/galleries/examples/text_labels_and_annotations/demo_annotation_box.py index e6c21bd69107..43e92f0570e9 100644 --- a/galleries/examples/text_labels_and_annotations/demo_annotation_box.py +++ b/galleries/examples/text_labels_and_annotations/demo_annotation_box.py @@ -1,89 +1,99 @@ """ -=================== -AnnotationBbox demo -=================== - -`.AnnotationBbox` creates an annotation using an `.OffsetBox`, and -provides more fine-grained control than `.Axes.annotate`. This example -demonstrates the use of AnnotationBbox together with three different -OffsetBoxes: `.TextArea`, `.DrawingArea`, and `.OffsetImage`. +====================== +Artists as annotations +====================== + +`.AnnotationBbox` facilitates using arbitrary artists as annotations, i.e. data at +position *xy* is annotated by a box containing an artist at position *xybox*. The +coordinate systems for these points are set via the *xycoords* and *boxcoords* +parameters, respectively; see the *xycoords* and *textcoords* parameters of +`.Axes.annotate` for a full listing of supported coordinate systems. +The box containing the artist is a subclass of `.OffsetBox`, which is a container +artist for positioning an artist relative to a parent artist. """ +from pathlib import Path + +import PIL import matplotlib.pyplot as plt import numpy as np -from matplotlib.cbook import get_sample_data +from matplotlib import get_data_path from matplotlib.offsetbox import AnnotationBbox, DrawingArea, OffsetImage, TextArea -from matplotlib.patches import Circle +from matplotlib.patches import Annulus, Circle, ConnectionPatch -fig, ax = plt.subplots() +# %%%% +# Text +# ==== +# +# `.AnnotationBbox` supports positioning annotations relative to data, Artists, and +# callables, as described in :ref:`annotations`. The `.TextArea` is used to create a +# textbox that is not explicitly attached to an axes, which allows it to be used for +# annotating figure objects. When annotating an axes element (such as a plot) with text, +# use `.Axes.annotate` because it will create the text artist for you. -# Define a 1st position to annotate (display it with a marker) -xy = (0.5, 0.7) -ax.plot(xy[0], xy[1], ".r") +fig, axd = plt.subplot_mosaic([['t1', '.', 't2']], layout='compressed') -# Annotate the 1st position with a text box ('Test 1') +# Define a 1st position to annotate (display it with a marker) +xy1 = (.25, .75) +xy2 = (.75, .25) +axd['t1'].plot(*xy1, ".r") +axd['t2'].plot(*xy2, ".r") +axd['t1'].set(xlim=(0, 1), ylim=(0, 1), aspect='equal') +axd['t2'].set(xlim=(0, 1), ylim=(0, 1), aspect='equal') + +# Draw a connection patch arrow between the points +c = ConnectionPatch(xyA=xy1, xyB=xy2, + coordsA=axd['t1'].transData, coordsB=axd['t2'].transData, + arrowstyle='->') +fig.add_artist(c) + +# Annotate the ConnectionPatch position ('Test 1') offsetbox = TextArea("Test 1") -ab = AnnotationBbox(offsetbox, xy, - xybox=(-20, 40), - xycoords='data', - boxcoords="offset points", - arrowprops=dict(arrowstyle="->"), - bboxprops=dict(boxstyle="sawtooth")) -ax.add_artist(ab) - -# Annotate the 1st position with another text box ('Test') -offsetbox = TextArea("Test") - -ab = AnnotationBbox(offsetbox, xy, - xybox=(1.02, xy[1]), - xycoords='data', - boxcoords=("axes fraction", "data"), - box_alignment=(0., 0.5), - arrowprops=dict(arrowstyle="->")) -ax.add_artist(ab) - -# Define a 2nd position to annotate (don't display with a marker this time) -xy = [0.3, 0.55] - -# Annotate the 2nd position with a circle patch -da = DrawingArea(20, 20, 0, 0) -p = Circle((10, 10), 10) -da.add_artist(p) - -ab = AnnotationBbox(da, xy, - xybox=(1., xy[1]), - xycoords='data', - boxcoords=("axes fraction", "data"), - box_alignment=(0.2, 0.5), - arrowprops=dict(arrowstyle="->"), - bboxprops=dict(alpha=0.5)) +# place the annotation above the midpoint of c +ab1 = AnnotationBbox(offsetbox, + xy=(.5, .5), + xybox=(0, 30), + xycoords=c, + boxcoords="offset points", + arrowprops=dict(arrowstyle="->"), + bboxprops=dict(boxstyle="sawtooth")) +fig.add_artist(ab1) + +# %%%% +# Images +# ====== +# The `.OffsetImage` container facilitates using images as annotations -ax.add_artist(ab) +fig, ax = plt.subplots() +# Define a position to annotate +xy = (0.3, 0.55) +ax.scatter(*xy, s=200, marker='X') -# Annotate the 2nd position with an image (a generated array of pixels) +# Annotate a position with an image generated from an array of pixels arr = np.arange(100).reshape((10, 10)) -im = OffsetImage(arr, zoom=2) +im = OffsetImage(arr, zoom=2, cmap='viridis') im.image.axes = ax -ab = AnnotationBbox(im, xy, +# place the image NW of xy +ab = AnnotationBbox(im, xy=xy, xybox=(-50., 50.), xycoords='data', boxcoords="offset points", pad=0.3, arrowprops=dict(arrowstyle="->")) - ax.add_artist(ab) -# Annotate the 2nd position with another image (a Grace Hopper portrait) -with get_sample_data("grace_hopper.jpg") as file: - arr_img = plt.imread(file) +# Annotate the position with an image from file (a Grace Hopper portrait) +img_fp = Path(get_data_path(), "sample_data", "grace_hopper.jpg") +with PIL.Image.open(img_fp) as arr_img: + imagebox = OffsetImage(arr_img, zoom=0.2) -imagebox = OffsetImage(arr_img, zoom=0.2) imagebox.image.axes = ax -ab = AnnotationBbox(imagebox, xy, +# place the image SE of xy +ab = AnnotationBbox(imagebox, xy=xy, xybox=(120., -80.), xycoords='data', boxcoords="offset points", @@ -96,8 +106,45 @@ ax.add_artist(ab) # Fix the display limits to see everything -ax.set_xlim(0, 1) -ax.set_ylim(0, 1) +ax.set(xlim=(0, 1), ylim=(0, 1)) + +plt.show() + +# %%%% +# Arbitrary Artists +# ================= +# +# Multiple and arbitrary artists can be placed inside a `.DrawingArea`. + +# make this the thumbnail image +# sphinx_gallery_thumbnail_number = 3 +fig, ax = plt.subplots() + +# Define a position to annotate +xy = (0.05, 0.5) +ax.scatter(*xy, s=500, marker='X') + +# Annotate the position with a circle and annulus +da = DrawingArea(120, 120) +p = Circle((30, 30), 25, color='C0') +da.add_artist(p) +q = Annulus((65, 65), 50, 5, color='C1') +da.add_artist(q) + + +# Use the drawing area as an annotation +ab = AnnotationBbox(da, xy=xy, + xybox=(.55, xy[1]), + xycoords='data', + boxcoords=("axes fraction", "data"), + box_alignment=(0, 0.5), + arrowprops=dict(arrowstyle="->"), + bboxprops=dict(alpha=0.5)) + +ax.add_artist(ab) + +# Fix the display limits to see everything +ax.set(xlim=(0, 1), ylim=(0, 1)) plt.show() @@ -108,11 +155,10 @@ # The use of the following functions, methods, classes and modules is shown # in this example: # -# - `matplotlib.patches.Circle` # - `matplotlib.offsetbox.TextArea` # - `matplotlib.offsetbox.DrawingArea` # - `matplotlib.offsetbox.OffsetImage` # - `matplotlib.offsetbox.AnnotationBbox` -# - `matplotlib.cbook.get_sample_data` -# - `matplotlib.pyplot.subplots` -# - `matplotlib.pyplot.imread` +# +# .. tags:: +# component: annotation, styling: position diff --git a/galleries/users_explain/text/annotations.py b/galleries/users_explain/text/annotations.py index b0eff8d19f7d..7c25aa9eed9a 100644 --- a/galleries/users_explain/text/annotations.py +++ b/galleries/users_explain/text/annotations.py @@ -697,6 +697,50 @@ def __call__(self, x0, y0, width, height, mutation_size): # Note that, unlike in `.Legend`, the ``bbox_transform`` is set to # `.IdentityTransform` by default # +# .. _annotations-bbox: +# +# Using an Artist as an annotation +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +# `.AnnotationBbox` uses artists in `.OffsetBox` container artists as the annotations +# and supports positioning these annotations using the same coordinate systems as the +# other annotation methods. For more examples, see +# :doc:`/gallery/text_labels_and_annotations/demo_annotation_box` + +from matplotlib.offsetbox import AnnotationBbox, DrawingArea, OffsetImage +from matplotlib.patches import Annulus + +fig, ax = plt.subplots() + +text = ax.text(.2, .8, "Green!", color='green') + +da = DrawingArea(20, 20) +annulus = Annulus((10, 10), 10, 5, color='tab:green') +da.add_artist(annulus) + +# position annulus relative to text +ab1 = AnnotationBbox(da, xy=(.5, 0), + xybox=(.5, .25), + xycoords=text, + boxcoords=(text, "data"), + arrowprops=dict(arrowstyle="->"), + bboxprops=dict(alpha=0.5)) +ax.add_artist(ab1) + +N = 25 +arr = np.repeat(np.linspace(0, 1, N), N).reshape(N, N) +im = OffsetImage(arr, cmap='Greens') +im.image.axes = ax + +# position gradient relative to text and annulus +ab2 = AnnotationBbox(im, xy=(.5, 0), + xybox=(.75, 0), + xycoords=text, + boxcoords=('data', annulus), + arrowprops=dict(arrowstyle="->"), + bboxprops=dict(alpha=0.5)) +ax.add_artist(ab2) + +# %%%% # .. _annotating_coordinate_systems: # # Coordinate systems for annotations diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py index c1846f92608c..483526fcd0a0 100644 --- a/lib/matplotlib/image.py +++ b/lib/matplotlib/image.py @@ -1404,9 +1404,9 @@ class BboxImage(_ImageBase): cmap : str or `~matplotlib.colors.Colormap`, default: :rc:`image.cmap` The Colormap instance or registered colormap name used to map scalar - data to colors. + data to colors. This parameter is ignored if X is RGB(A). norm : str or `~matplotlib.colors.Normalize` - Maps luminance to 0-1. + Maps luminance to 0-1. This parameter is ignored if X is RGB(A). interpolation : str, default: :rc:`image.interpolation` Supported values are 'none', 'auto', 'nearest', 'bilinear', 'bicubic', 'spline16', 'spline36', 'hanning', 'hamming', 'hermite', diff --git a/lib/matplotlib/offsetbox.py b/lib/matplotlib/offsetbox.py index a410a6cfe990..9b9c7a69f35f 100644 --- a/lib/matplotlib/offsetbox.py +++ b/lib/matplotlib/offsetbox.py @@ -1155,7 +1155,70 @@ def __init__(self, s, loc, *, pad=0.4, borderpad=0.5, prop=None, **kwargs): class OffsetImage(OffsetBox): + """ + Container artist for images. + + Image data is displayed using `.BboxImage`. This image is meant to be positioned + relative to a parent artist. + + Parameters + ---------- + arr: array-like or `PIL.Image.Image` + The data to be color-coded. The interpretation depends on the + shape: + + - (M, N) `~numpy.ndarray` or masked array: values to be colormapped + - (M, N, 3): RGB array + - (M, N, 4): RGBA array + + zoom: float, default: 1 + zoom factor: + + - no zoom: factor =1 + - zoom in: factor > 1 + - zoom out: 0< factor < 1 + + cmap : str or `~matplotlib.colors.Colormap`, default: :rc:`image.cmap` + The Colormap instance or registered colormap name used to map scalar + data to colors. This parameter is ignored if X is RGB(A). + + norm : str or `~matplotlib.colors.Normalize`, default: None + Maps luminance to 0-1. This parameter is ignored if X is RGB(A). + interpolation : str, default: :rc:`image.interpolation` + Supported values are 'none', 'auto', 'nearest', 'bilinear', + 'bicubic', 'spline16', 'spline36', 'hanning', 'hamming', 'hermite', + 'kaiser', 'quadric', 'catrom', 'gaussian', 'bessel', 'mitchell', + 'sinc', 'lanczos', 'blackman'. + + origin : {'upper', 'lower'}, default: :rc:`image.origin` + Place the [0, 0] index of the array in the upper left or lower left + corner of the Axes. The convention 'upper' is typically used for + matrices and images. + + filternorm : bool, default: True + A parameter for the antigrain image resize filter + (see the antigrain documentation). + If filternorm is set, the filter normalizes integer values and corrects + the rounding errors. It doesn't do anything with the source floating + point values, it corrects only integers according to the rule of 1.0 + which means that any sum of pixel weights must be equal to 1.0. So, + the filter function must produce a graph of the proper shape. + + filterrad : float > 0, default: 4 + The filter radius for filters that have a radius parameter, i.e. when + interpolation is one of: 'sinc', 'lanczos' or 'blackman'. + + resample : bool, default: False + When True, use a full resampling method. When False, only resample when + the output image is larger than the input image. + + dpi_cor: bool, default: True + Correct for the backend DPI setting + + **kwargs : `.BboxImage` properties + + """ def __init__(self, arr, *, zoom=1, cmap=None, @@ -1168,7 +1231,6 @@ def __init__(self, arr, *, dpi_cor=True, **kwargs ): - super().__init__() self._dpi_cor = dpi_cor