Skip to content

VectorGlyph Class#

The VectorGlyph class renders a 2-D (u, v) vector field over (x, y) positions as arrows (quiver), wind barbs (barbs), or streamlines (streamplot). The artist is coloured by the per-vector magnitude hypot(u, v) through the shared scalar-mapping pipeline, with a matching colorbar.

VectorGlyph(..., thin=n) draws every nth grid point for quiver / barbs, and plot(compose=True) lays the arrows over an existing layer instead of replacing it — see Render options.

Class Documentation#

cleopatra.glyphs.gridded.vector_glyph.VectorGlyph #

Bases: GeoMixin, Glyph

Visualization class for 2D vector fields.

Renders a (u, v) vector field over (x, y) positions as arrows, wind barbs, or streamlines, with the artist coloured by the vector magnitude hypot(u, v) through the shared scalar-mapping pipeline.

Parameters:

Name Type Description Default
x ndarray

x-coordinates of the vector positions.

required
y ndarray

y-coordinates of the vector positions.

required
u ndarray

x-components of the vectors. Must broadcast against x/y.

required
v ndarray

y-components of the vectors. Must broadcast against x/y.

required
ax Axes | None

Pre-existing axes to draw on. Default is None.

None
fig Figure | None

Pre-existing figure. Default is None.

None
**kwargs

Override any key in VECTOR_DEFAULT_OPTIONS (e.g. density, scale, cmap, vmin, vmax, levels, color_scale, ticks_spacing, cbar_label, figsize, title). Set add_colorbar=False to suppress the per-glyph colorbar (default True) where the host owns a single aggregated colorbar; plot(compose=True), which keeps the host's own layers, already suppresses it by default, and add_colorbar=True here is how a composed overlay asks for one back. Set thin=n to draw every nth grid point for quiver/barbs, which a real grid needs (see VectorGlyph._thinned).

{}

Examples:

  • Build a field and inspect the stored magnitude:
    >>> import numpy as np
    >>> from cleopatra.glyphs.gridded.vector_glyph import VectorGlyph
    >>> x, y = np.meshgrid(np.arange(2), np.arange(2))
    >>> u = np.array([[3.0, 0.0], [0.0, 3.0]])
    >>> v = np.array([[4.0, 0.0], [0.0, 4.0]])
    >>> glyph = VectorGlyph(x, y, u, v)
    >>> float(glyph.magnitude.max())
    5.0
    
See Also

cleopatra.glyphs.base.glyph.Glyph._prepare_scalar_mapping: Shared norm/colorbar/ticks pipeline used to colour by magnitude.

Source code in src/cleopatra/glyphs/gridded/vector_glyph.py
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
class VectorGlyph(GeoMixin, Glyph):
    """Visualization class for 2D vector fields.

    Renders a `(u, v)` vector field over `(x, y)` positions as arrows,
    wind barbs, or streamlines, with the artist coloured by the vector
    magnitude `hypot(u, v)` through the shared scalar-mapping pipeline.

    Args:
        x: x-coordinates of the vector positions.
        y: y-coordinates of the vector positions.
        u: x-components of the vectors. Must broadcast against `x`/`y`.
        v: y-components of the vectors. Must broadcast against `x`/`y`.
        ax: Pre-existing axes to draw on. Default is None.
        fig: Pre-existing figure. Default is None.
        **kwargs: Override any key in `VECTOR_DEFAULT_OPTIONS`
            (e.g. `density`, `scale`, `cmap`, `vmin`, `vmax`, `levels`,
            `color_scale`, `ticks_spacing`, `cbar_label`, `figsize`,
            `title`). Set `add_colorbar=False` to suppress the per-glyph
            colorbar (default True) where the host owns a single aggregated
            colorbar; `plot(compose=True)`, which keeps the host's own
            layers, already suppresses it by default, and
            `add_colorbar=True` here is how a composed overlay asks for one
            back. Set `thin=n` to draw every nth grid point for
            `quiver`/`barbs`, which a real grid needs (see
            `VectorGlyph._thinned`).

    Examples:
        - Build a field and inspect the stored magnitude:
            ```python
            >>> import numpy as np
            >>> from cleopatra.glyphs.gridded.vector_glyph import VectorGlyph
            >>> x, y = np.meshgrid(np.arange(2), np.arange(2))
            >>> u = np.array([[3.0, 0.0], [0.0, 3.0]])
            >>> v = np.array([[4.0, 0.0], [0.0, 4.0]])
            >>> glyph = VectorGlyph(x, y, u, v)
            >>> float(glyph.magnitude.max())
            5.0

            ```

    See Also:
        cleopatra.glyphs.base.glyph.Glyph._prepare_scalar_mapping: Shared
            norm/colorbar/ticks pipeline used to colour by magnitude.
    """

    #: Option keys this glyph accepts (see `Glyph.option_keys`/`filter_kwargs`).
    DEFAULT_OPTIONS = VECTOR_DEFAULT_OPTIONS

    def __init__(
        self,
        x: np.ndarray,
        y: np.ndarray,
        u: np.ndarray,
        v: np.ndarray,
        *,
        ax: Axes | None = None,
        fig: Figure | None = None,
        **kwargs,
    ):
        super().__init__(
            default_options=VECTOR_DEFAULT_OPTIONS, fig=fig, ax=ax, **kwargs
        )
        self.x, self.y, self.u, self.v = (np.asarray(a) for a in (x, y, u, v))
        if self.u.shape != self.v.shape:
            raise ValueError(
                f"u and v must have the same shape, got {self.u.shape} "
                f"and {self.v.shape}."
            )
        self.cbar: Colorbar | None = None
        #: The `Quiver`/`Barbs`/streamplot `LineCollection` mappable from
        #: the most recent `plot` call; `None` before first render.
        self.im: Any = None

    @property
    def magnitude(self) -> np.ndarray:
        """Per-vector magnitude `hypot(u, v)` used for colour mapping."""
        return np.asarray(np.hypot(self.u, self.v))

    def _thinned(
        self, thin: int, mag: np.ndarray
    ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
        """Return the field subsampled to every `thin`th grid point.

        `quiver` and `barbs` draw one arrow per point, which on a real grid is
        both unreadable and slow -- a 141x321 window is 45,261 arrows. Thinning
        here means a caller does not have to subsample the data and rebuild a
        coarser grid themselves.

        Args:
            thin: Keep every `thin`th point along each axis. `1` keeps all.
            mag: The magnitude array, thinned alongside so the colours still
                line up with the arrows.

        Returns:
            tuple: `(x, y, u, v, magnitude)`, each subsampled.

        `thin` is validated by `_validate_thin` before the render begins, so
        that an invalid value never reaches the point where artists have already
        been cleared.
        """

        def take(array: np.ndarray) -> np.ndarray:
            """Subsample one array along however many axes it has.

            1-D coordinate vectors index on their only axis; a meshgrid indexes
            on both, so the slice is built from the array's own dimensionality
            rather than assumed.

            Args:
                array: The array to subsample.

            Returns:
                np.ndarray: Every `thin`th element along each axis.
            """
            values = np.asarray(array)
            step = (slice(None, None, thin),) * values.ndim
            return values[step]

        return (
            take(self.x),
            take(self.y),
            take(self.u),
            take(self.v),
            take(mag),
        )

    def plot(
        self,
        kind: str = "quiver",
        ax: Axes | None = None,
        title: str | None = None,
        add_colorbar: bool | None = None,
        colorbar: bool | ColorBar | None = None,
        color: ColorScaling | None = None,
        contour: Contour | None = None,
        classify: Classify | None = None,
        compose: bool = False,
    ):
        """Render the vector field, coloured by magnitude.

        Dispatches to `Axes.quiver`, `Axes.barbs`, or `Axes.streamplot`
        based on `kind`. The colour scale, norm, ticks, and colorbar are
        resolved from the magnitude via `_prepare_scalar_mapping`.

        Args:
            kind: One of `"quiver"`, `"barbs"`, or `"streamplot"`.
                Default is `"quiver"`.
            ax: Axes to draw on. Falls back to the axes supplied at
                construction, otherwise a new figure/axes is created.
            title: Plot title. Overrides `default_options["title"]`
                when given.
            add_colorbar: Override the `add_colorbar` option for this call
                — True draws the colorbar, False suppresses it. Defaults to
                None, which leaves the decision to the `add_colorbar` option
                (set at construction, `True` by default) -- except under
                `compose=True`, where that default flips off unless the caller
                asked for a bar through `colorbar=` or a construction-time
                `add_colorbar=`.
            colorbar: Typed `ColorBar` spec (or `True`/`False`/`None`) for the
                colorbar's placement, caption, and sizing; resolved into the
                `cbar_*` options. A `ColorBar`/`True` also enables the bar and is
                **sticky** -- it persists into later plots, overriding a
                construction-time `add_colorbar=False`; an explicit
                `add_colorbar=` argument still wins the on/off decision. Under
                `compose=True`, anything but `None` here counts as asking for
                the overlay's own colorbar, which is otherwise off.
            compose: Draw *over* whatever is already on `ax` instead of
                replacing it, leaving another glyph's layers and colorbar
                intact. Off by default, where a render replaces every glyph's
                artists on the axes (see issue #210). Turn it on to lay one
                field over another -- arrows on a scalar background. The arrows
                then bring **no colorbar of their own** by default: the bar
                would take its space from the host axes, shrinking the raster it
                is drawn over. Ask for one with `add_colorbar=True`, `colorbar=`
                or a construction-time `add_colorbar=True` if the overlay's
                magnitude needs its own scale.

        Returns:
            tuple[Figure, Axes, Any]: The figure, the axes, and the
                mappable artist (the `Quiver`, `Barbs`, or the
                streamplot's `LineCollection`) that the colorbar is
                attached to.

        Raises:
            ValueError: If `kind` is not a recognised vector kind, or if
                the magnitude has no finite values (via
                `_prepare_scalar_mapping`).

        Examples:
            - A barbs plot returns the Barbs mappable with the magnitude
                array:
                ```python
                >>> import numpy as np
                >>> from cleopatra.glyphs.gridded.vector_glyph import VectorGlyph
                >>> x, y = np.meshgrid(np.arange(3), np.arange(3))
                >>> u = np.full_like(x, 2.0, dtype=float)
                >>> v = np.zeros_like(y, dtype=float)
                >>> glyph = VectorGlyph(x, y, u, v)
                >>> fig, ax, im = glyph.plot(kind="barbs")
                >>> float(im.get_array().max())
                2.0

                ```
            - Arrows composed over a host raster draw on the host's own axes and
                add no colorbar, so the figure keeps the one axes it had:
                ```python
                >>> import matplotlib
                >>> matplotlib.use("Agg")
                >>> import matplotlib.pyplot as plt
                >>> import numpy as np
                >>> from cleopatra.glyphs.gridded.vector_glyph import VectorGlyph
                >>> host_fig, host_ax = plt.subplots()
                >>> _ = host_ax.imshow(np.arange(9.0).reshape(3, 3), extent=[0, 2, 0, 2])
                >>> x, y = np.meshgrid(np.arange(3), np.arange(3))
                >>> u = np.full_like(x, 1.0, dtype=float)
                >>> v = np.full_like(y, 1.0, dtype=float)
                >>> fig, ax, im = VectorGlyph(x, y, u, v).plot(
                ...     kind="quiver", ax=host_ax, compose=True
                ... )
                >>> len(fig.axes)
                1
                >>> ax is host_ax
                True
                >>> plt.close(host_fig)

                ```
            - Asking for the overlay's colorbar brings it back:
                ```python
                >>> import matplotlib
                >>> matplotlib.use("Agg")
                >>> import matplotlib.pyplot as plt
                >>> import numpy as np
                >>> from cleopatra.glyphs.gridded.vector_glyph import VectorGlyph
                >>> host_fig, host_ax = plt.subplots()
                >>> _ = host_ax.imshow(np.arange(9.0).reshape(3, 3), extent=[0, 2, 0, 2])
                >>> x, y = np.meshgrid(np.arange(3), np.arange(3))
                >>> u = np.full_like(x, 1.0, dtype=float)
                >>> v = np.full_like(y, 1.0, dtype=float)
                >>> fig, ax, im = VectorGlyph(x, y, u, v).plot(
                ...     kind="quiver", ax=host_ax, compose=True, add_colorbar=True
                ... )
                >>> len(fig.axes)
                2
                >>> plt.close(host_fig)

                ```
            - An unknown kind raises ValueError:
                ```python
                >>> import numpy as np
                >>> from cleopatra.glyphs.gridded.vector_glyph import VectorGlyph
                >>> x, y = np.meshgrid(np.arange(2), np.arange(2))
                >>> glyph = VectorGlyph(x, y, np.ones_like(x), np.ones_like(y))
                >>> glyph.plot(kind="spirograph")
                Traceback (most recent call last):
                    ...
                ValueError: unknown vector kind 'spirograph'; expected one of ...

                ```
        """
        if kind not in VECTOR_KINDS:
            raise ValueError(
                f"unknown vector kind {kind!r}; expected one of "
                f"{', '.join(VECTOR_KINDS)}."
            )

        with self._rollback_options_on_error():
            self._merge_group_params(color, contour, classify)

            if ax is not None:
                self.ax = ax
                self.fig = _root_figure(ax)
            elif self.ax is None:
                self.fig, self.ax = self.create_figure_axes()
            ax = self.ax
            opts = self.default_options

            if title is not None:
                opts["title"] = title
            opts.update(_resolve_colorbar(colorbar))
            draw_colorbar = (
                self._draws_own_colorbar(compose, colorbar)
                if add_colorbar is None
                else add_colorbar
            )

            mag = self.magnitude
            norm, cbar_kw, ticks = self._prepare_scalar_mapping(mag)
            cmap = resolve_colormap(opts["cmap"])
            clim = {} if norm else {"clim": (ticks[0], ticks[-1])}

            # Validate before clearing: a bad `thin` used to raise only once the
            # host's artists were already gone, leaving a wiped axes behind.
            _validate_thin(opts["thin"], kind)
            _clear_prior_render_artists(ax, self, compose=compose)
            self.im = None
            self.cbar = None

            im, arrow_patches = self._draw_field(ax, kind, mag, cmap, norm, ticks, clim)

            self.im = im
            if draw_colorbar:
                self.cbar = self.create_color_bar(ax, im, cbar_kw)

            if opts["title"]:
                ax.set_title(opts["title"], fontsize=opts["title_size"])
            self._apply_axis_style(ax)

            _mark_render_artists(ax, self, self.cbar, self.im, *arrow_patches)
            return self.fig, ax, im

    def _draw_field(
        self,
        ax: Axes,
        kind: str,
        mag: np.ndarray,
        cmap: Any,
        norm: Any,
        ticks: np.ndarray,
        clim: dict,
    ) -> tuple[Any, tuple]:
        """Create the artists for one `kind` and return them.

        Split out of `plot` so that method reads as the option-resolution and
        bookkeeping it mostly is, with the three matplotlib calls -- and the two
        `streamplot`-only fix-ups -- in one place.

        Args:
            ax: The axes to draw on.
            kind: One of `"quiver"`, `"barbs"` or `"streamplot"`; already
                validated by the caller.
            mag: The per-vector magnitude the artist is coloured by.
            cmap: The resolved colormap.
            norm: The resolved norm, or `None` when the caller passes an
                explicit `clim` instead.
            ticks: The colorbar tick positions, used for that explicit `clim`.
            clim: `{"clim": (low, high)}` when there is no norm, else `{}`.

        Returns:
            tuple[Any, tuple]: The mappable to hand the colorbar, and the arrow
            patches `streamplot` adds directly to the axes (empty for the other
            two kinds, which return a single artist).
        """
        opts = self.default_options
        if kind == "streamplot":
            patches_before = set(ax.patches)
            stream = ax.streamplot(
                self.x,
                self.y,
                self.u,
                self.v,
                color=mag,
                cmap=cmap,
                norm=norm,
                density=opts["density"],
            )
            im = stream.lines
            # `streamplot` colours its own segments and does not always leave an
            # array behind for the colorbar to read.
            if im.get_array() is None:
                im.set_array(np.asarray(mag).ravel())
            if norm is None:
                im.set_clim(ticks[0], ticks[-1])
            return im, tuple(set(ax.patches) - patches_before)

        x, y, u, v, arrow_mag = self._thinned(opts["thin"], mag)
        if kind == "quiver":
            return (
                ax.quiver(
                    x,
                    y,
                    u,
                    v,
                    arrow_mag,
                    cmap=cmap,
                    norm=norm,
                    scale=opts["scale"],
                    **clim,
                ),
                (),
            )
        return (
            ax.barbs(x, y, u, v, arrow_mag, cmap=cmap, norm=norm, **clim),
            (),
        )

    def add_key(
        self,
        im,
        x: float = 0.9,
        y: float = 1.02,
        value: float = 10.0,
        label: str | None = None,
        labelpos: str = "E",
        **kwargs,
    ) -> QuiverKey:
        """Add a reference-arrow key to a quiver plot.

        Wraps `Axes.quiverkey` to draw a sample arrow of known length
        with a text label, the standard legend for a quiver field.

        Args:
            im: The `Quiver` artist returned by `plot(kind="quiver")`.
            x: Key x-position in axes fraction coordinates.
                Default is 0.9.
            y: Key y-position in axes fraction coordinates.
                Default is 1.02.
            value: The reference vector length the key represents.
                Default is 10.0.
            label: Text drawn beside the key. Default is `None`, which
                renders the numeric `value` as the label.
            labelpos: Side of the arrow for the label (`"N"`, `"S"`,
                `"E"`, `"W"`). Default is `"E"`.
            **kwargs: Forwarded to `Axes.quiverkey`.

        Returns:
            QuiverKey: The created key artist.

        Examples:
            - Add a 5 m/s reference key to a quiver:
                ```python
                >>> import numpy as np
                >>> from cleopatra.glyphs.gridded.vector_glyph import VectorGlyph
                >>> x, y = np.meshgrid(np.arange(3), np.arange(3))
                >>> u = np.ones_like(x, dtype=float)
                >>> v = np.ones_like(y, dtype=float)
                >>> glyph = VectorGlyph(x, y, u, v)
                >>> fig, ax, im = glyph.plot(kind="quiver")
                >>> key = glyph.add_key(im, value=5.0, label="5 m/s")
                >>> key.text.get_text()
                '5 m/s'

                ```
        """
        text = label if label is not None else f"{value:g}"
        key: QuiverKey = self.ax.quiverkey(
            im, x, y, value, text, labelpos=labelpos, **kwargs
        )
        return key

magnitude property #

Per-vector magnitude hypot(u, v) used for colour mapping.

add_key(im, x=0.9, y=1.02, value=10.0, label=None, labelpos='E', **kwargs) #

Add a reference-arrow key to a quiver plot.

Wraps Axes.quiverkey to draw a sample arrow of known length with a text label, the standard legend for a quiver field.

Parameters:

Name Type Description Default
im

The Quiver artist returned by plot(kind="quiver").

required
x float

Key x-position in axes fraction coordinates. Default is 0.9.

0.9
y float

Key y-position in axes fraction coordinates. Default is 1.02.

1.02
value float

The reference vector length the key represents. Default is 10.0.

10.0
label str | None

Text drawn beside the key. Default is None, which renders the numeric value as the label.

None
labelpos str

Side of the arrow for the label ("N", "S", "E", "W"). Default is "E".

'E'
**kwargs

Forwarded to Axes.quiverkey.

{}

Returns:

Name Type Description
QuiverKey QuiverKey

The created key artist.

Examples:

  • Add a 5 m/s reference key to a quiver:
    >>> import numpy as np
    >>> from cleopatra.glyphs.gridded.vector_glyph import VectorGlyph
    >>> x, y = np.meshgrid(np.arange(3), np.arange(3))
    >>> u = np.ones_like(x, dtype=float)
    >>> v = np.ones_like(y, dtype=float)
    >>> glyph = VectorGlyph(x, y, u, v)
    >>> fig, ax, im = glyph.plot(kind="quiver")
    >>> key = glyph.add_key(im, value=5.0, label="5 m/s")
    >>> key.text.get_text()
    '5 m/s'
    
Source code in src/cleopatra/glyphs/gridded/vector_glyph.py
def add_key(
    self,
    im,
    x: float = 0.9,
    y: float = 1.02,
    value: float = 10.0,
    label: str | None = None,
    labelpos: str = "E",
    **kwargs,
) -> QuiverKey:
    """Add a reference-arrow key to a quiver plot.

    Wraps `Axes.quiverkey` to draw a sample arrow of known length
    with a text label, the standard legend for a quiver field.

    Args:
        im: The `Quiver` artist returned by `plot(kind="quiver")`.
        x: Key x-position in axes fraction coordinates.
            Default is 0.9.
        y: Key y-position in axes fraction coordinates.
            Default is 1.02.
        value: The reference vector length the key represents.
            Default is 10.0.
        label: Text drawn beside the key. Default is `None`, which
            renders the numeric `value` as the label.
        labelpos: Side of the arrow for the label (`"N"`, `"S"`,
            `"E"`, `"W"`). Default is `"E"`.
        **kwargs: Forwarded to `Axes.quiverkey`.

    Returns:
        QuiverKey: The created key artist.

    Examples:
        - Add a 5 m/s reference key to a quiver:
            ```python
            >>> import numpy as np
            >>> from cleopatra.glyphs.gridded.vector_glyph import VectorGlyph
            >>> x, y = np.meshgrid(np.arange(3), np.arange(3))
            >>> u = np.ones_like(x, dtype=float)
            >>> v = np.ones_like(y, dtype=float)
            >>> glyph = VectorGlyph(x, y, u, v)
            >>> fig, ax, im = glyph.plot(kind="quiver")
            >>> key = glyph.add_key(im, value=5.0, label="5 m/s")
            >>> key.text.get_text()
            '5 m/s'

            ```
    """
    text = label if label is not None else f"{value:g}"
    key: QuiverKey = self.ax.quiverkey(
        im, x, y, value, text, labelpos=labelpos, **kwargs
    )
    return key

plot(kind='quiver', ax=None, title=None, add_colorbar=None, colorbar=None, color=None, contour=None, classify=None, compose=False) #

Render the vector field, coloured by magnitude.

Dispatches to Axes.quiver, Axes.barbs, or Axes.streamplot based on kind. The colour scale, norm, ticks, and colorbar are resolved from the magnitude via _prepare_scalar_mapping.

Parameters:

Name Type Description Default
kind str

One of "quiver", "barbs", or "streamplot". Default is "quiver".

'quiver'
ax Axes | None

Axes to draw on. Falls back to the axes supplied at construction, otherwise a new figure/axes is created.

None
title str | None

Plot title. Overrides default_options["title"] when given.

None
add_colorbar bool | None

Override the add_colorbar option for this call — True draws the colorbar, False suppresses it. Defaults to None, which leaves the decision to the add_colorbar option (set at construction, True by default) -- except under compose=True, where that default flips off unless the caller asked for a bar through colorbar= or a construction-time add_colorbar=.

None
colorbar bool | ColorBar | None

Typed ColorBar spec (or True/False/None) for the colorbar's placement, caption, and sizing; resolved into the cbar_* options. A ColorBar/True also enables the bar and is sticky -- it persists into later plots, overriding a construction-time add_colorbar=False; an explicit add_colorbar= argument still wins the on/off decision. Under compose=True, anything but None here counts as asking for the overlay's own colorbar, which is otherwise off.

None
compose bool

Draw over whatever is already on ax instead of replacing it, leaving another glyph's layers and colorbar intact. Off by default, where a render replaces every glyph's artists on the axes (see issue #210). Turn it on to lay one field over another -- arrows on a scalar background. The arrows then bring no colorbar of their own by default: the bar would take its space from the host axes, shrinking the raster it is drawn over. Ask for one with add_colorbar=True, colorbar= or a construction-time add_colorbar=True if the overlay's magnitude needs its own scale.

False

Returns:

Type Description

tuple[Figure, Axes, Any]: The figure, the axes, and the mappable artist (the Quiver, Barbs, or the streamplot's LineCollection) that the colorbar is attached to.

Raises:

Type Description
ValueError

If kind is not a recognised vector kind, or if the magnitude has no finite values (via _prepare_scalar_mapping).

Examples:

  • A barbs plot returns the Barbs mappable with the magnitude array:
    >>> import numpy as np
    >>> from cleopatra.glyphs.gridded.vector_glyph import VectorGlyph
    >>> x, y = np.meshgrid(np.arange(3), np.arange(3))
    >>> u = np.full_like(x, 2.0, dtype=float)
    >>> v = np.zeros_like(y, dtype=float)
    >>> glyph = VectorGlyph(x, y, u, v)
    >>> fig, ax, im = glyph.plot(kind="barbs")
    >>> float(im.get_array().max())
    2.0
    
  • Arrows composed over a host raster draw on the host's own axes and add no colorbar, so the figure keeps the one axes it had:
    >>> import matplotlib
    >>> matplotlib.use("Agg")
    >>> import matplotlib.pyplot as plt
    >>> import numpy as np
    >>> from cleopatra.glyphs.gridded.vector_glyph import VectorGlyph
    >>> host_fig, host_ax = plt.subplots()
    >>> _ = host_ax.imshow(np.arange(9.0).reshape(3, 3), extent=[0, 2, 0, 2])
    >>> x, y = np.meshgrid(np.arange(3), np.arange(3))
    >>> u = np.full_like(x, 1.0, dtype=float)
    >>> v = np.full_like(y, 1.0, dtype=float)
    >>> fig, ax, im = VectorGlyph(x, y, u, v).plot(
    ...     kind="quiver", ax=host_ax, compose=True
    ... )
    >>> len(fig.axes)
    1
    >>> ax is host_ax
    True
    >>> plt.close(host_fig)
    
  • Asking for the overlay's colorbar brings it back:
    >>> import matplotlib
    >>> matplotlib.use("Agg")
    >>> import matplotlib.pyplot as plt
    >>> import numpy as np
    >>> from cleopatra.glyphs.gridded.vector_glyph import VectorGlyph
    >>> host_fig, host_ax = plt.subplots()
    >>> _ = host_ax.imshow(np.arange(9.0).reshape(3, 3), extent=[0, 2, 0, 2])
    >>> x, y = np.meshgrid(np.arange(3), np.arange(3))
    >>> u = np.full_like(x, 1.0, dtype=float)
    >>> v = np.full_like(y, 1.0, dtype=float)
    >>> fig, ax, im = VectorGlyph(x, y, u, v).plot(
    ...     kind="quiver", ax=host_ax, compose=True, add_colorbar=True
    ... )
    >>> len(fig.axes)
    2
    >>> plt.close(host_fig)
    
  • An unknown kind raises ValueError:
    >>> import numpy as np
    >>> from cleopatra.glyphs.gridded.vector_glyph import VectorGlyph
    >>> x, y = np.meshgrid(np.arange(2), np.arange(2))
    >>> glyph = VectorGlyph(x, y, np.ones_like(x), np.ones_like(y))
    >>> glyph.plot(kind="spirograph")
    Traceback (most recent call last):
        ...
    ValueError: unknown vector kind 'spirograph'; expected one of ...
    
Source code in src/cleopatra/glyphs/gridded/vector_glyph.py
def plot(
    self,
    kind: str = "quiver",
    ax: Axes | None = None,
    title: str | None = None,
    add_colorbar: bool | None = None,
    colorbar: bool | ColorBar | None = None,
    color: ColorScaling | None = None,
    contour: Contour | None = None,
    classify: Classify | None = None,
    compose: bool = False,
):
    """Render the vector field, coloured by magnitude.

    Dispatches to `Axes.quiver`, `Axes.barbs`, or `Axes.streamplot`
    based on `kind`. The colour scale, norm, ticks, and colorbar are
    resolved from the magnitude via `_prepare_scalar_mapping`.

    Args:
        kind: One of `"quiver"`, `"barbs"`, or `"streamplot"`.
            Default is `"quiver"`.
        ax: Axes to draw on. Falls back to the axes supplied at
            construction, otherwise a new figure/axes is created.
        title: Plot title. Overrides `default_options["title"]`
            when given.
        add_colorbar: Override the `add_colorbar` option for this call
            — True draws the colorbar, False suppresses it. Defaults to
            None, which leaves the decision to the `add_colorbar` option
            (set at construction, `True` by default) -- except under
            `compose=True`, where that default flips off unless the caller
            asked for a bar through `colorbar=` or a construction-time
            `add_colorbar=`.
        colorbar: Typed `ColorBar` spec (or `True`/`False`/`None`) for the
            colorbar's placement, caption, and sizing; resolved into the
            `cbar_*` options. A `ColorBar`/`True` also enables the bar and is
            **sticky** -- it persists into later plots, overriding a
            construction-time `add_colorbar=False`; an explicit
            `add_colorbar=` argument still wins the on/off decision. Under
            `compose=True`, anything but `None` here counts as asking for
            the overlay's own colorbar, which is otherwise off.
        compose: Draw *over* whatever is already on `ax` instead of
            replacing it, leaving another glyph's layers and colorbar
            intact. Off by default, where a render replaces every glyph's
            artists on the axes (see issue #210). Turn it on to lay one
            field over another -- arrows on a scalar background. The arrows
            then bring **no colorbar of their own** by default: the bar
            would take its space from the host axes, shrinking the raster it
            is drawn over. Ask for one with `add_colorbar=True`, `colorbar=`
            or a construction-time `add_colorbar=True` if the overlay's
            magnitude needs its own scale.

    Returns:
        tuple[Figure, Axes, Any]: The figure, the axes, and the
            mappable artist (the `Quiver`, `Barbs`, or the
            streamplot's `LineCollection`) that the colorbar is
            attached to.

    Raises:
        ValueError: If `kind` is not a recognised vector kind, or if
            the magnitude has no finite values (via
            `_prepare_scalar_mapping`).

    Examples:
        - A barbs plot returns the Barbs mappable with the magnitude
            array:
            ```python
            >>> import numpy as np
            >>> from cleopatra.glyphs.gridded.vector_glyph import VectorGlyph
            >>> x, y = np.meshgrid(np.arange(3), np.arange(3))
            >>> u = np.full_like(x, 2.0, dtype=float)
            >>> v = np.zeros_like(y, dtype=float)
            >>> glyph = VectorGlyph(x, y, u, v)
            >>> fig, ax, im = glyph.plot(kind="barbs")
            >>> float(im.get_array().max())
            2.0

            ```
        - Arrows composed over a host raster draw on the host's own axes and
            add no colorbar, so the figure keeps the one axes it had:
            ```python
            >>> import matplotlib
            >>> matplotlib.use("Agg")
            >>> import matplotlib.pyplot as plt
            >>> import numpy as np
            >>> from cleopatra.glyphs.gridded.vector_glyph import VectorGlyph
            >>> host_fig, host_ax = plt.subplots()
            >>> _ = host_ax.imshow(np.arange(9.0).reshape(3, 3), extent=[0, 2, 0, 2])
            >>> x, y = np.meshgrid(np.arange(3), np.arange(3))
            >>> u = np.full_like(x, 1.0, dtype=float)
            >>> v = np.full_like(y, 1.0, dtype=float)
            >>> fig, ax, im = VectorGlyph(x, y, u, v).plot(
            ...     kind="quiver", ax=host_ax, compose=True
            ... )
            >>> len(fig.axes)
            1
            >>> ax is host_ax
            True
            >>> plt.close(host_fig)

            ```
        - Asking for the overlay's colorbar brings it back:
            ```python
            >>> import matplotlib
            >>> matplotlib.use("Agg")
            >>> import matplotlib.pyplot as plt
            >>> import numpy as np
            >>> from cleopatra.glyphs.gridded.vector_glyph import VectorGlyph
            >>> host_fig, host_ax = plt.subplots()
            >>> _ = host_ax.imshow(np.arange(9.0).reshape(3, 3), extent=[0, 2, 0, 2])
            >>> x, y = np.meshgrid(np.arange(3), np.arange(3))
            >>> u = np.full_like(x, 1.0, dtype=float)
            >>> v = np.full_like(y, 1.0, dtype=float)
            >>> fig, ax, im = VectorGlyph(x, y, u, v).plot(
            ...     kind="quiver", ax=host_ax, compose=True, add_colorbar=True
            ... )
            >>> len(fig.axes)
            2
            >>> plt.close(host_fig)

            ```
        - An unknown kind raises ValueError:
            ```python
            >>> import numpy as np
            >>> from cleopatra.glyphs.gridded.vector_glyph import VectorGlyph
            >>> x, y = np.meshgrid(np.arange(2), np.arange(2))
            >>> glyph = VectorGlyph(x, y, np.ones_like(x), np.ones_like(y))
            >>> glyph.plot(kind="spirograph")
            Traceback (most recent call last):
                ...
            ValueError: unknown vector kind 'spirograph'; expected one of ...

            ```
    """
    if kind not in VECTOR_KINDS:
        raise ValueError(
            f"unknown vector kind {kind!r}; expected one of "
            f"{', '.join(VECTOR_KINDS)}."
        )

    with self._rollback_options_on_error():
        self._merge_group_params(color, contour, classify)

        if ax is not None:
            self.ax = ax
            self.fig = _root_figure(ax)
        elif self.ax is None:
            self.fig, self.ax = self.create_figure_axes()
        ax = self.ax
        opts = self.default_options

        if title is not None:
            opts["title"] = title
        opts.update(_resolve_colorbar(colorbar))
        draw_colorbar = (
            self._draws_own_colorbar(compose, colorbar)
            if add_colorbar is None
            else add_colorbar
        )

        mag = self.magnitude
        norm, cbar_kw, ticks = self._prepare_scalar_mapping(mag)
        cmap = resolve_colormap(opts["cmap"])
        clim = {} if norm else {"clim": (ticks[0], ticks[-1])}

        # Validate before clearing: a bad `thin` used to raise only once the
        # host's artists were already gone, leaving a wiped axes behind.
        _validate_thin(opts["thin"], kind)
        _clear_prior_render_artists(ax, self, compose=compose)
        self.im = None
        self.cbar = None

        im, arrow_patches = self._draw_field(ax, kind, mag, cmap, norm, ticks, clim)

        self.im = im
        if draw_colorbar:
            self.cbar = self.create_color_bar(ax, im, cbar_kw)

        if opts["title"]:
            ax.set_title(opts["title"], fontsize=opts["title_size"])
        self._apply_axis_style(ax)

        _mark_render_artists(ax, self, self.cbar, self.im, *arrow_patches)
        return self.fig, ax, im

Examples#

Arrows (quiver)#

import numpy as np
from cleopatra.glyphs.gridded.vector_glyph import VectorGlyph

x, y = np.meshgrid(np.linspace(0, 1, 8), np.linspace(0, 1, 8))
u, v = np.cos(x * np.pi), np.sin(y * np.pi)

vg = VectorGlyph(x, y, u, v)
fig, ax, artist = vg.plot(kind="quiver", title="Vector field")

Wind barbs and streamlines#

fig, ax, barbs = vg.plot(kind="barbs")
fig, ax, stream = vg.plot(kind="streamplot")

Adding a reference key#

fig, ax, quiv = vg.plot(kind="quiver")
vg.add_key(quiv, 0.9, 0.95, 1.0, "1 m/s")