Skip to content

Render options (grouped parameters)#

Glyph plot() / animate() calls take these typed objects in place of loose keyword arguments. Each bundles a family of related options and exposes to_options(), which the glyph flattens into its render settings — only the fields you set are applied, so a group never clobbers a glyph's own defaults. (The ArrayGlyph-specific input objects — RgbBands, PointOverlay, FrameLabel, PanelLabels — are documented on the ArrayGlyph page.)

ColorScaling#

The colour-scale (norm) selector: plot(color=ColorScaling.power(gamma=0.5)), ColorScaling.sym_log(...), ColorScaling.boundary(bounds=[...]), ColorScaling.midpoint(at=0), ColorScaling.linear().

cleopatra.styling.scaling.ColorScaling dataclass #

The colour-scale group: a scale kind plus its scale-specific knobs.

Prefer the variant constructors (linear, power, sym_log, log, boundary, midpoint, equalize) over the raw dataclass -- each exposes only the fields its scale uses, so nonsensical combinations (e.g. a midpoint on a linear scale) cannot be built.

Attributes:

Name Type Description
kind ColorScale

The scale kind (cleopatra.styling.styles.ColorScale).

gamma float

Exponent for the power scale. Ignored by other kinds.

line_threshold float | None

Linear-region threshold (linthresh) for sym-lognorm. None (the default) auto-derives it from the data range at render time; an explicit value is used as given.

line_scale float | None

Linear-region scale factor (linscale) for sym-lognorm. None (the default) pairs a sensible width (matplotlib's 1.0) with an auto-derived line_threshold; an explicit value is used as given.

bounds list[float] | None

Explicit bin edges for boundary-norm.

center float

Centre value for the midpoint scale (the value pinned to the colormap centre). Named center rather than midpoint so the field does not shadow the midpoint() variant constructor.

samples int

Number of quantile samples for the equalize scale -- the resolution of the empirical-CDF table. Ignored by other kinds.

Source code in src/cleopatra/styling/scaling.py
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
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
@dataclass(frozen=True)
class ColorScaling:
    """The colour-scale group: a scale kind plus its scale-specific knobs.

    Prefer the variant constructors (`linear`, `power`, `sym_log`, `log`,
    `boundary`, `midpoint`, `equalize`) over the raw dataclass -- each exposes
    only the fields its scale uses, so nonsensical combinations (e.g. a
    `midpoint` on a `linear` scale) cannot be built.

    Attributes:
        kind: The scale kind (`cleopatra.styling.styles.ColorScale`).
        gamma: Exponent for the `power` scale. Ignored by other kinds.
        line_threshold: Linear-region threshold (`linthresh`) for
            `sym-lognorm`. `None` (the default) auto-derives it from the data
            range at render time; an explicit value is used as given.
        line_scale: Linear-region scale factor (`linscale`) for `sym-lognorm`.
            `None` (the default) pairs a sensible width (matplotlib's `1.0`)
            with an auto-derived `line_threshold`; an explicit value is used as
            given.
        bounds: Explicit bin edges for `boundary-norm`.
        center: Centre value for the `midpoint` scale (the value pinned to
            the colormap centre). Named `center` rather than `midpoint` so
            the field does not shadow the `midpoint()` variant constructor.
        samples: Number of quantile samples for the `equalize` scale -- the
            resolution of the empirical-CDF table. Ignored by other kinds.
    """

    kind: ColorScale = ColorScale.LINEAR
    gamma: float = 0.5
    line_threshold: float | None = None
    line_scale: float | None = None
    bounds: list[float] | None = None
    center: float = 0
    samples: int = 512

    @classmethod
    def linear(cls) -> ColorScaling:
        """A plain linear colour scale (matplotlib's default norm).

        Examples:
            - The linear scale carries no extra knobs:
                ```python
                >>> from cleopatra.styling.scaling import ColorScaling
                >>> ColorScaling.linear().kind.value
                'linear'

                ```
        """
        return cls(kind=ColorScale.LINEAR)

    @classmethod
    def power(cls, gamma: float = 0.5) -> ColorScaling:
        """A power-law (`PowerNorm`) colour scale.

        Args:
            gamma: The power exponent. Defaults to `0.5`.

        Examples:
            - Only `gamma` is exposed:
                ```python
                >>> from cleopatra.styling.scaling import ColorScaling
                >>> ColorScaling.power(gamma=2.0).gamma
                2.0

                ```
        """
        return cls(kind=ColorScale.POWER, gamma=gamma)

    @classmethod
    def sym_log(
        cls, threshold: float | None = None, scale: float | None = None
    ) -> ColorScaling:
        """A symmetric-log (`SymLogNorm`) colour scale.

        Args:
            threshold: The linear-region half-width (`linthresh`) -- the
                boundary between the linear band around zero and the log tail.
                Defaults to `None`, which auto-derives it from the data range at
                render time (a small fraction of the data's peak magnitude), so
                the log decades track the data's own scale instead of running
                far below it. Pass an explicit value to pin the band near a
                scale you care about; an explicit `threshold` always wins over
                the auto-derivation.
            scale: The linear-region scale factor (`linscale`) -- how much
                colour-bar width the linear band around zero occupies. Defaults
                to `None`, which pairs a sensible width (matplotlib's `1.0`)
                with the auto-derived `threshold` so the widened linear band
                stays legible. Pass an explicit value to override it; an
                explicit `scale` always wins.

        Examples:
            - Exposes the two `sym-lognorm` knobs:
                ```python
                >>> from cleopatra.styling.scaling import ColorScaling
                >>> s = ColorScaling.sym_log(threshold=0.01, scale=0.1)
                >>> (s.line_threshold, s.line_scale)
                (0.01, 0.1)

                ```
            - The default defers the threshold to the data range:
                ```python
                >>> ColorScaling.sym_log().line_threshold is None
                True

                ```
        """
        return cls(kind=ColorScale.SYM_LOGNORM, line_threshold=threshold, line_scale=scale)

    @classmethod
    def log(cls) -> ColorScaling:
        """A logarithmic (`LogNorm`) colour scale for strictly-positive data.

        The plain-log counterpart of `sym_log`: `LogNorm` needs a positive
        value range, so for data that spans zero or negative values use
        `sym_log` (a symmetric-log scale) instead. Like `linear`, it carries
        no extra knobs -- `vmin`/`vmax` come from the tick range at render
        time.

        On `ArrayGlyph`, an un-pinned `vmin` is floored at the smallest positive
        value that is not an extreme low outlier (`ArrayGlyph._log_safe_vmin`),
        so a lone near-zero pixel does not drag the bar's decades below the
        data's bulk (issue #339); pass an explicit `vmin` to keep the raw
        minimum.

        Examples:
            - The log scale exposes no extra knobs:
                ```python
                >>> from cleopatra.styling.scaling import ColorScaling
                >>> ColorScaling.log().kind.value
                'lognorm'

                ```
        """
        return cls(kind=ColorScale.LOGNORM)

    @classmethod
    def boundary(cls, bounds: list[float] | None = None) -> ColorScaling:
        """A discrete (`BoundaryNorm`) colour scale.

        Args:
            bounds: Explicit bin edges. When `None`, the edges are derived
                from `levels` (if set) or the tick positions at render
                time.

        Examples:
            - Explicit edges are carried through:
                ```python
                >>> from cleopatra.styling.scaling import ColorScaling
                >>> ColorScaling.boundary([0, 1, 5, 10]).bounds
                [0, 1, 5, 10]

                ```
        """
        return cls(kind=ColorScale.BOUNDARY_NORM, bounds=bounds)

    @classmethod
    def midpoint(cls, at: float = 0) -> ColorScaling:
        """A midpoint-anchored diverging colour scale.

        Args:
            at: The value pinned to the colormap centre. Defaults to `0`.

        Examples:
            - Anchor the colormap centre at a chosen value:
                ```python
                >>> from cleopatra.styling.scaling import ColorScaling
                >>> ColorScaling.midpoint(at=100).center
                100

                ```
        """
        return cls(kind=ColorScale.MIDPOINT, center=at)

    @classmethod
    def equalize(cls, samples: int = 512) -> ColorScaling:
        """A continuous rank-equalising colour scale (histogram equalisation).

        Spreads the colour ramp by rank rather than by value, so every quantile
        of the data receives an equal share of the ramp. On a skewed field
        (bathymetry, population, discharge) this reveals the bulk that a linear
        norm flattens into one tone -- and, unlike `boundary`, it stays
        continuous, so it does not posterise a shaded-relief surface. It is
        backed by a `matplotlib.colors.FuncNorm` built from the data's own
        empirical CDF at render time.

        It ranks within the resolved display window, so `vmin`/`vmax` and
        `robust=True` clip the field before ranking (handy for taming outliers
        on a skewed surface); with no limits it ranks the whole field.

        The scale is data-driven, so it is wired for `ArrayGlyph` (which can
        supply its cell values); using it where the values are unavailable
        raises a clear error rather than guessing.

        Args:
            samples: Number of quantile samples in the empirical-CDF table --
                its resolution. Must be at least 2. Defaults to `512`.

        Raises:
            ValueError: If `samples` is less than 2.

        Examples:
            - The equalize scale carries its sample count:
                ```python
                >>> from cleopatra.styling.scaling import ColorScaling
                >>> ColorScaling.equalize().kind.value
                'equalize'
                >>> ColorScaling.equalize(samples=256).samples
                256

                ```
        """
        if samples < 2:
            raise ValueError(f"equalize needs samples >= 2, got {samples}.")
        return cls(kind=ColorScale.EQUALIZE, samples=samples)

    @classmethod
    def from_options(cls, options: dict[str, Any]) -> ColorScaling:
        """Build a `ColorScaling` from a flat `default_options` dict.

        The bridge between the legacy flat-key storage every glyph still
        uses internally and this object's behaviour. Reads the six
        colour-scale keys, validating `color_scale` with the same
        actionable error the flat path raised.

        Args:
            options: A glyph's `default_options` (or any mapping carrying
                the colour-scale keys).

        Returns:
            ColorScaling: The reconstructed scale object.

        Raises:
            ValueError: If `options["color_scale"]` is not a recognised
                `cleopatra.styling.styles.ColorScale` value.

        Examples:
            - Round-trips the flat keys back into an object:
                ```python
                >>> from cleopatra.styling.scaling import ColorScaling
                >>> ColorScaling.from_options({"color_scale": "power", "gamma": 0.7}).gamma
                0.7

                ```
        """
        raw_scale = options.get("color_scale", _SCALE_DEFAULTS["color_scale"])
        try:
            kind = ColorScale(raw_scale)
        except ValueError as e:
            valid = ", ".join(repr(m.value) for m in ColorScale)
            raise ValueError(
                f"Invalid color_scale {raw_scale!r}. Expected one of "
                f"{valid} (or a cleopatra.styling.styles.ColorScale member)."
            ) from e
        return cls(
            kind=kind,
            gamma=options.get("gamma", _SCALE_DEFAULTS["gamma"]),
            line_threshold=options.get("line_threshold", _SCALE_DEFAULTS["line_threshold"]),
            line_scale=options.get("line_scale", _SCALE_DEFAULTS["line_scale"]),
            bounds=options.get("bounds", _SCALE_DEFAULTS["bounds"]),
            center=options.get("midpoint", _SCALE_DEFAULTS["midpoint"]),
            samples=options.get("samples", _SCALE_DEFAULTS["samples"]),
        )

    def to_options(self) -> dict[str, Any]:
        """Flatten back to the `default_options` keys the engine reads.

        Returns:
            dict: The colour-scale keys, with `color_scale` as the plain
                string value and `norm` reset to `None` (a scale clears any
                raw-norm escape hatch).

        Examples:
            - Emits the flat keys a glyph merges into `default_options`:
                ```python
                >>> from cleopatra.styling.scaling import ColorScaling
                >>> ColorScaling.power(gamma=0.7).to_options()["color_scale"]
                'power'

                ```
        """
        return {
            "color_scale": self.kind.value,
            "gamma": self.gamma,
            "line_threshold": self.line_threshold,
            "line_scale": self.line_scale,
            "bounds": self.bounds,
            "midpoint": self.center,
            "samples": self.samples,
            # A scale is a full reset: choosing one clears any raw-norm escape
            # hatch (`plot(norm=...)`) so a later `color=ColorScaling.*` is not
            # silently shadowed by a sticky caller norm.
            "norm": None,
        }

    def build_norm(
        self,
        ticks: np.ndarray,
        levels: int | list[float] | np.ndarray | None = None,
        extend: str | None = None,
        values: np.ndarray | None = None,
    ) -> tuple[colors.Normalize | None, dict[str, Any]]:
        """Build the matplotlib norm and colorbar keyword arguments.

        The colour-scale logic that used to live in
        `Glyph._create_norm_and_cbar_kw`. `vmin`/`vmax` are read from the
        first and last tick; `levels` and `extend` are cross-group inputs
        (contour discretisation and colorbar arrow extension) passed in by
        the caller.

        Args:
            ticks: Tick positions for the colorbar; `ticks[0]`/`ticks[-1]`
                supply `vmin`/`vmax`.
            levels: Optional discretisation for the `linear`/`boundary`
                kinds (int count or explicit edges).
            extend: Colorbar arrow extension. When `None`, auto-resolves to
                `"both"` if `levels` is set, else `"neither"`.
            values: The data's own values, used only by the `equalize` scale
                to build its empirical-CDF table. `None` (the default) is fine
                for every other kind; `equalize` raises when it is `None`.

        Returns:
            tuple[Normalize or None, dict]: The norm (`None` for a plain
                linear scale) and the colorbar keyword arguments.

        Examples:
            - A linear scale with no levels yields no norm and passes the
                ticks straight through:
                ```python
                >>> import numpy as np
                >>> from cleopatra.styling.scaling import ColorScaling
                >>> norm, cbar_kw = ColorScaling.linear().build_norm(
                ...     np.array([0.0, 5.0, 10.0])
                ... )
                >>> norm is None
                True
                >>> cbar_kw["extend"]
                'neither'

                ```
            - `levels` on the linear scale builds a `BoundaryNorm` and
                defaults `extend` to `"both"`:
                ```python
                >>> import numpy as np
                >>> from cleopatra.styling.scaling import ColorScaling
                >>> norm, cbar_kw = ColorScaling.linear().build_norm(
                ...     np.array([0.0, 5.0, 10.0]), levels=5
                ... )
                >>> norm is None
                False
                >>> cbar_kw["extend"]
                'both'

                ```
        """
        vmin = ticks[0]
        vmax = ticks[-1]
        bounds_from_levels = levels_to_bounds(levels, vmin, vmax)

        norm: colors.Normalize | None
        cbar_kw: dict[str, Any]
        if self.kind == ColorScale.LINEAR:
            norm, cbar_kw = self._linear_norm(ticks, bounds_from_levels)
        elif self.kind == ColorScale.POWER:
            norm = colors.PowerNorm(gamma=self.gamma, vmin=vmin, vmax=vmax)
            cbar_kw = {"ticks": ticks}
        elif self.kind == ColorScale.SYM_LOGNORM:
            norm, cbar_kw = self._sym_log_norm(ticks, vmin, vmax)
        elif self.kind == ColorScale.LOGNORM:
            norm, cbar_kw = self._log_norm(ticks, vmin, vmax)
        elif self.kind == ColorScale.BOUNDARY_NORM:
            norm, cbar_kw = self._boundary_norm(ticks, bounds_from_levels)
        elif self.kind == ColorScale.MIDPOINT:
            norm = MidpointNormalize(midpoint=self.center, vmin=vmin, vmax=vmax)
            cbar_kw = {"ticks": ticks}
        elif self.kind == ColorScale.EQUALIZE:
            norm, cbar_kw = self._equalize_norm(ticks, values)
        else:  # pragma: no cover - a ColorScale member without a branch
            raise ValueError(
                f"No norm branch implemented for color_scale={self.kind!r}."
            )

        if extend is None:
            extend = "both" if levels is not None else "neither"
        cbar_kw["extend"] = extend
        return norm, cbar_kw

    def _linear_norm(
        self, ticks: np.ndarray, bounds_from_levels: np.ndarray | None
    ) -> tuple[colors.Normalize | None, dict[str, Any]]:
        """Linear-scale norm: a `BoundaryNorm` when `levels` are given, else no norm."""
        if bounds_from_levels is not None:
            norm = colors.BoundaryNorm(boundaries=bounds_from_levels, ncolors=256)
            return norm, {"ticks": bounds_from_levels}
        return None, {"ticks": ticks}

    def _sym_log_norm(
        self, ticks: np.ndarray, vmin: Any, vmax: Any
    ) -> tuple[colors.Normalize, dict[str, Any]]:
        """Symmetric-log norm, deriving the linear band from the data when unset.

        A `None` threshold means "match the data": derive `linthresh` from the
        range so the log decades stay near the data's scale instead of running
        arbitrarily far below it (issue #337). The same value drives the norm
        (the rendered image) and the bar ticks, so they stay consistent. A `None`
        scale likewise pairs matplotlib's `1.0` `linscale` with that wider band,
        so the near-zero region keeps a legible share of the bar and its in-band
        ticks don't overprint. An explicit `threshold`/`scale` is used as given.
        """
        linthresh = (
            _auto_linthresh(vmin, vmax)
            if self.line_threshold is None
            else self.line_threshold
        )
        linscale = _AUTO_LINSCALE if self.line_scale is None else self.line_scale
        norm = colors.SymLogNorm(
            linthresh=linthresh, linscale=linscale, base=np.e, vmin=vmin, vmax=vmax
        )
        cbar_kw = {
            "ticks": _symlog_tick_positions(vmin, vmax, linthresh, ticks),
            "format": _plain_tick_formatter(),
        }
        return norm, cbar_kw

    def _log_norm(
        self, ticks: np.ndarray, vmin: Any, vmax: Any
    ) -> tuple[colors.Normalize, dict[str, Any]]:
        """Plain-log norm over a strictly-positive range, widening a constant field.

        A constant *positive* field yields a single tick (`vmin == vmax`); a log
        scale cannot span a zero-width range, so widen it -- matching the
        data-style `norm='log'` path, which bumps `vmax = vmin + 1.0`. Only widen
        a positive constant: a non-positive one must raise, and its error should
        report the real bound, not a widened one.
        """
        lo, hi = float(vmin), float(vmax)
        if hi == lo and lo > 0.0:
            hi = lo + 1.0
        norm = build_log_norm(
            lo, hi, context="ColorScaling.log()", remedy="use ColorScaling.sym_log()"
        )
        cbar_kw = {
            "ticks": _log_tick_positions(lo, hi, ticks),
            "format": _plain_tick_formatter(),
        }
        return norm, cbar_kw

    def _boundary_norm(
        self, ticks: np.ndarray, bounds_from_levels: np.ndarray | None
    ) -> tuple[colors.Normalize, dict[str, Any]]:
        """Explicit-bounds norm: own `bounds` win, then `levels`, then the ticks."""
        if self.bounds:
            bounds = self.bounds
        elif bounds_from_levels is not None:
            bounds = bounds_from_levels
        else:
            bounds = ticks
        return colors.BoundaryNorm(boundaries=bounds, ncolors=256), {"ticks": bounds}

    def _equalize_norm(
        self, ticks: np.ndarray, values: np.ndarray | None
    ) -> tuple[colors.Normalize, dict[str, Any]]:
        """Rank-equalising norm: a `FuncNorm` over the data's own empirical CDF.

        Maps each value to its quantile rank in `[0, 1]`, so every quantile of
        the data gets an equal share of the ramp. Needs the data itself (not
        just the tick range), so `values` is required; the colour bar's ticks
        are placed at the data's quantiles rather than linearly, so they sit
        evenly on the equalised axis instead of implying a linear one.

        Ranks within the resolved display window `[ticks[0], ticks[-1]]`, so an
        explicit `vmin`/`vmax` or `robust=True` clips the field before ranking
        (out-of-window outliers then take the end colours rather than flattening
        the in-window distribution). The default window is the data range, so it
        keeps every cell.
        """
        if values is None:
            raise ValueError(
                "ColorScaling.equalize() needs the data values to build its "
                "quantile table. It is wired for ArrayGlyph (which supplies its "
                "cell values); pass values= to build_norm() to use it directly."
            )
        data = np.asarray(values, dtype=float)
        data = data[np.isfinite(data)]
        if data.size == 0:
            raise ValueError("ColorScaling.equalize() got no finite values to rank.")
        if ticks is not None and len(ticks) >= 2:
            lo_lim, hi_lim = float(ticks[0]), float(ticks[-1])
            if hi_lim > lo_lim:
                in_window = data[(data >= lo_lim) & (data <= hi_lim)]
                if in_window.size:
                    data = in_window
        q = np.linspace(0.0, 1.0, self.samples)
        qv = np.quantile(data, q)
        # A flat plateau repeats a data value across several quantiles, giving
        # np.interp a zero-width interval; keep a strictly increasing support by
        # dropping the repeats (np.unique returns sorted-unique + first index).
        qv_unique, first = np.unique(qv, return_index=True)
        q_unique = q[first]
        if qv_unique.size < 2:
            # A constant / fully-tied field has no rank spread to apply: fall
            # back to a degenerate linear norm rather than dividing by zero.
            lo = float(qv_unique[0])
            return colors.Normalize(vmin=lo, vmax=lo), {"ticks": np.array([lo])}
        lo, hi = float(qv_unique[0]), float(qv_unique[-1])
        norm = colors.FuncNorm(
            (
                lambda x, xp=qv_unique, fp=q_unique: np.interp(x, xp, fp),
                lambda y, xp=q_unique, fp=qv_unique: np.interp(y, xp, fp),
            ),
            vmin=lo,
            vmax=hi,
        )
        n_ticks = len(ticks) if ticks is not None and len(ticks) >= 2 else 8
        # Reuse the CDF table (qv) rather than a second np.quantile sort of the
        # full field; interpolating it at the tick quantiles gives the same
        # quantile-spaced positions.
        tick_vals = np.unique(np.interp(np.linspace(0.0, 1.0, n_ticks), q, qv))
        return norm, {"ticks": tick_vals, "format": _plain_tick_formatter()}

boundary(bounds=None) classmethod #

A discrete (BoundaryNorm) colour scale.

Parameters:

Name Type Description Default
bounds list[float] | None

Explicit bin edges. When None, the edges are derived from levels (if set) or the tick positions at render time.

None

Examples:

  • Explicit edges are carried through:
    >>> from cleopatra.styling.scaling import ColorScaling
    >>> ColorScaling.boundary([0, 1, 5, 10]).bounds
    [0, 1, 5, 10]
    
Source code in src/cleopatra/styling/scaling.py
@classmethod
def boundary(cls, bounds: list[float] | None = None) -> ColorScaling:
    """A discrete (`BoundaryNorm`) colour scale.

    Args:
        bounds: Explicit bin edges. When `None`, the edges are derived
            from `levels` (if set) or the tick positions at render
            time.

    Examples:
        - Explicit edges are carried through:
            ```python
            >>> from cleopatra.styling.scaling import ColorScaling
            >>> ColorScaling.boundary([0, 1, 5, 10]).bounds
            [0, 1, 5, 10]

            ```
    """
    return cls(kind=ColorScale.BOUNDARY_NORM, bounds=bounds)

build_norm(ticks, levels=None, extend=None, values=None) #

Build the matplotlib norm and colorbar keyword arguments.

The colour-scale logic that used to live in Glyph._create_norm_and_cbar_kw. vmin/vmax are read from the first and last tick; levels and extend are cross-group inputs (contour discretisation and colorbar arrow extension) passed in by the caller.

Parameters:

Name Type Description Default
ticks ndarray

Tick positions for the colorbar; ticks[0]/ticks[-1] supply vmin/vmax.

required
levels int | list[float] | ndarray | None

Optional discretisation for the linear/boundary kinds (int count or explicit edges).

None
extend str | None

Colorbar arrow extension. When None, auto-resolves to "both" if levels is set, else "neither".

None
values ndarray | None

The data's own values, used only by the equalize scale to build its empirical-CDF table. None (the default) is fine for every other kind; equalize raises when it is None.

None

Returns:

Type Description
tuple[Normalize | None, dict[str, Any]]

tuple[Normalize or None, dict]: The norm (None for a plain linear scale) and the colorbar keyword arguments.

Examples:

  • A linear scale with no levels yields no norm and passes the ticks straight through:
    >>> import numpy as np
    >>> from cleopatra.styling.scaling import ColorScaling
    >>> norm, cbar_kw = ColorScaling.linear().build_norm(
    ...     np.array([0.0, 5.0, 10.0])
    ... )
    >>> norm is None
    True
    >>> cbar_kw["extend"]
    'neither'
    
  • levels on the linear scale builds a BoundaryNorm and defaults extend to "both":
    >>> import numpy as np
    >>> from cleopatra.styling.scaling import ColorScaling
    >>> norm, cbar_kw = ColorScaling.linear().build_norm(
    ...     np.array([0.0, 5.0, 10.0]), levels=5
    ... )
    >>> norm is None
    False
    >>> cbar_kw["extend"]
    'both'
    
Source code in src/cleopatra/styling/scaling.py
def build_norm(
    self,
    ticks: np.ndarray,
    levels: int | list[float] | np.ndarray | None = None,
    extend: str | None = None,
    values: np.ndarray | None = None,
) -> tuple[colors.Normalize | None, dict[str, Any]]:
    """Build the matplotlib norm and colorbar keyword arguments.

    The colour-scale logic that used to live in
    `Glyph._create_norm_and_cbar_kw`. `vmin`/`vmax` are read from the
    first and last tick; `levels` and `extend` are cross-group inputs
    (contour discretisation and colorbar arrow extension) passed in by
    the caller.

    Args:
        ticks: Tick positions for the colorbar; `ticks[0]`/`ticks[-1]`
            supply `vmin`/`vmax`.
        levels: Optional discretisation for the `linear`/`boundary`
            kinds (int count or explicit edges).
        extend: Colorbar arrow extension. When `None`, auto-resolves to
            `"both"` if `levels` is set, else `"neither"`.
        values: The data's own values, used only by the `equalize` scale
            to build its empirical-CDF table. `None` (the default) is fine
            for every other kind; `equalize` raises when it is `None`.

    Returns:
        tuple[Normalize or None, dict]: The norm (`None` for a plain
            linear scale) and the colorbar keyword arguments.

    Examples:
        - A linear scale with no levels yields no norm and passes the
            ticks straight through:
            ```python
            >>> import numpy as np
            >>> from cleopatra.styling.scaling import ColorScaling
            >>> norm, cbar_kw = ColorScaling.linear().build_norm(
            ...     np.array([0.0, 5.0, 10.0])
            ... )
            >>> norm is None
            True
            >>> cbar_kw["extend"]
            'neither'

            ```
        - `levels` on the linear scale builds a `BoundaryNorm` and
            defaults `extend` to `"both"`:
            ```python
            >>> import numpy as np
            >>> from cleopatra.styling.scaling import ColorScaling
            >>> norm, cbar_kw = ColorScaling.linear().build_norm(
            ...     np.array([0.0, 5.0, 10.0]), levels=5
            ... )
            >>> norm is None
            False
            >>> cbar_kw["extend"]
            'both'

            ```
    """
    vmin = ticks[0]
    vmax = ticks[-1]
    bounds_from_levels = levels_to_bounds(levels, vmin, vmax)

    norm: colors.Normalize | None
    cbar_kw: dict[str, Any]
    if self.kind == ColorScale.LINEAR:
        norm, cbar_kw = self._linear_norm(ticks, bounds_from_levels)
    elif self.kind == ColorScale.POWER:
        norm = colors.PowerNorm(gamma=self.gamma, vmin=vmin, vmax=vmax)
        cbar_kw = {"ticks": ticks}
    elif self.kind == ColorScale.SYM_LOGNORM:
        norm, cbar_kw = self._sym_log_norm(ticks, vmin, vmax)
    elif self.kind == ColorScale.LOGNORM:
        norm, cbar_kw = self._log_norm(ticks, vmin, vmax)
    elif self.kind == ColorScale.BOUNDARY_NORM:
        norm, cbar_kw = self._boundary_norm(ticks, bounds_from_levels)
    elif self.kind == ColorScale.MIDPOINT:
        norm = MidpointNormalize(midpoint=self.center, vmin=vmin, vmax=vmax)
        cbar_kw = {"ticks": ticks}
    elif self.kind == ColorScale.EQUALIZE:
        norm, cbar_kw = self._equalize_norm(ticks, values)
    else:  # pragma: no cover - a ColorScale member without a branch
        raise ValueError(
            f"No norm branch implemented for color_scale={self.kind!r}."
        )

    if extend is None:
        extend = "both" if levels is not None else "neither"
    cbar_kw["extend"] = extend
    return norm, cbar_kw

equalize(samples=512) classmethod #

A continuous rank-equalising colour scale (histogram equalisation).

Spreads the colour ramp by rank rather than by value, so every quantile of the data receives an equal share of the ramp. On a skewed field (bathymetry, population, discharge) this reveals the bulk that a linear norm flattens into one tone -- and, unlike boundary, it stays continuous, so it does not posterise a shaded-relief surface. It is backed by a matplotlib.colors.FuncNorm built from the data's own empirical CDF at render time.

It ranks within the resolved display window, so vmin/vmax and robust=True clip the field before ranking (handy for taming outliers on a skewed surface); with no limits it ranks the whole field.

The scale is data-driven, so it is wired for ArrayGlyph (which can supply its cell values); using it where the values are unavailable raises a clear error rather than guessing.

Parameters:

Name Type Description Default
samples int

Number of quantile samples in the empirical-CDF table -- its resolution. Must be at least 2. Defaults to 512.

512

Raises:

Type Description
ValueError

If samples is less than 2.

Examples:

  • The equalize scale carries its sample count:
    >>> from cleopatra.styling.scaling import ColorScaling
    >>> ColorScaling.equalize().kind.value
    'equalize'
    >>> ColorScaling.equalize(samples=256).samples
    256
    
Source code in src/cleopatra/styling/scaling.py
@classmethod
def equalize(cls, samples: int = 512) -> ColorScaling:
    """A continuous rank-equalising colour scale (histogram equalisation).

    Spreads the colour ramp by rank rather than by value, so every quantile
    of the data receives an equal share of the ramp. On a skewed field
    (bathymetry, population, discharge) this reveals the bulk that a linear
    norm flattens into one tone -- and, unlike `boundary`, it stays
    continuous, so it does not posterise a shaded-relief surface. It is
    backed by a `matplotlib.colors.FuncNorm` built from the data's own
    empirical CDF at render time.

    It ranks within the resolved display window, so `vmin`/`vmax` and
    `robust=True` clip the field before ranking (handy for taming outliers
    on a skewed surface); with no limits it ranks the whole field.

    The scale is data-driven, so it is wired for `ArrayGlyph` (which can
    supply its cell values); using it where the values are unavailable
    raises a clear error rather than guessing.

    Args:
        samples: Number of quantile samples in the empirical-CDF table --
            its resolution. Must be at least 2. Defaults to `512`.

    Raises:
        ValueError: If `samples` is less than 2.

    Examples:
        - The equalize scale carries its sample count:
            ```python
            >>> from cleopatra.styling.scaling import ColorScaling
            >>> ColorScaling.equalize().kind.value
            'equalize'
            >>> ColorScaling.equalize(samples=256).samples
            256

            ```
    """
    if samples < 2:
        raise ValueError(f"equalize needs samples >= 2, got {samples}.")
    return cls(kind=ColorScale.EQUALIZE, samples=samples)

from_options(options) classmethod #

Build a ColorScaling from a flat default_options dict.

The bridge between the legacy flat-key storage every glyph still uses internally and this object's behaviour. Reads the six colour-scale keys, validating color_scale with the same actionable error the flat path raised.

Parameters:

Name Type Description Default
options dict[str, Any]

A glyph's default_options (or any mapping carrying the colour-scale keys).

required

Returns:

Name Type Description
ColorScaling ColorScaling

The reconstructed scale object.

Raises:

Type Description
ValueError

If options["color_scale"] is not a recognised cleopatra.styling.styles.ColorScale value.

Examples:

  • Round-trips the flat keys back into an object:
    >>> from cleopatra.styling.scaling import ColorScaling
    >>> ColorScaling.from_options({"color_scale": "power", "gamma": 0.7}).gamma
    0.7
    
Source code in src/cleopatra/styling/scaling.py
@classmethod
def from_options(cls, options: dict[str, Any]) -> ColorScaling:
    """Build a `ColorScaling` from a flat `default_options` dict.

    The bridge between the legacy flat-key storage every glyph still
    uses internally and this object's behaviour. Reads the six
    colour-scale keys, validating `color_scale` with the same
    actionable error the flat path raised.

    Args:
        options: A glyph's `default_options` (or any mapping carrying
            the colour-scale keys).

    Returns:
        ColorScaling: The reconstructed scale object.

    Raises:
        ValueError: If `options["color_scale"]` is not a recognised
            `cleopatra.styling.styles.ColorScale` value.

    Examples:
        - Round-trips the flat keys back into an object:
            ```python
            >>> from cleopatra.styling.scaling import ColorScaling
            >>> ColorScaling.from_options({"color_scale": "power", "gamma": 0.7}).gamma
            0.7

            ```
    """
    raw_scale = options.get("color_scale", _SCALE_DEFAULTS["color_scale"])
    try:
        kind = ColorScale(raw_scale)
    except ValueError as e:
        valid = ", ".join(repr(m.value) for m in ColorScale)
        raise ValueError(
            f"Invalid color_scale {raw_scale!r}. Expected one of "
            f"{valid} (or a cleopatra.styling.styles.ColorScale member)."
        ) from e
    return cls(
        kind=kind,
        gamma=options.get("gamma", _SCALE_DEFAULTS["gamma"]),
        line_threshold=options.get("line_threshold", _SCALE_DEFAULTS["line_threshold"]),
        line_scale=options.get("line_scale", _SCALE_DEFAULTS["line_scale"]),
        bounds=options.get("bounds", _SCALE_DEFAULTS["bounds"]),
        center=options.get("midpoint", _SCALE_DEFAULTS["midpoint"]),
        samples=options.get("samples", _SCALE_DEFAULTS["samples"]),
    )

linear() classmethod #

A plain linear colour scale (matplotlib's default norm).

Examples:

  • The linear scale carries no extra knobs:
    >>> from cleopatra.styling.scaling import ColorScaling
    >>> ColorScaling.linear().kind.value
    'linear'
    
Source code in src/cleopatra/styling/scaling.py
@classmethod
def linear(cls) -> ColorScaling:
    """A plain linear colour scale (matplotlib's default norm).

    Examples:
        - The linear scale carries no extra knobs:
            ```python
            >>> from cleopatra.styling.scaling import ColorScaling
            >>> ColorScaling.linear().kind.value
            'linear'

            ```
    """
    return cls(kind=ColorScale.LINEAR)

log() classmethod #

A logarithmic (LogNorm) colour scale for strictly-positive data.

The plain-log counterpart of sym_log: LogNorm needs a positive value range, so for data that spans zero or negative values use sym_log (a symmetric-log scale) instead. Like linear, it carries no extra knobs -- vmin/vmax come from the tick range at render time.

On ArrayGlyph, an un-pinned vmin is floored at the smallest positive value that is not an extreme low outlier (ArrayGlyph._log_safe_vmin), so a lone near-zero pixel does not drag the bar's decades below the data's bulk (issue #339); pass an explicit vmin to keep the raw minimum.

Examples:

  • The log scale exposes no extra knobs:
    >>> from cleopatra.styling.scaling import ColorScaling
    >>> ColorScaling.log().kind.value
    'lognorm'
    
Source code in src/cleopatra/styling/scaling.py
@classmethod
def log(cls) -> ColorScaling:
    """A logarithmic (`LogNorm`) colour scale for strictly-positive data.

    The plain-log counterpart of `sym_log`: `LogNorm` needs a positive
    value range, so for data that spans zero or negative values use
    `sym_log` (a symmetric-log scale) instead. Like `linear`, it carries
    no extra knobs -- `vmin`/`vmax` come from the tick range at render
    time.

    On `ArrayGlyph`, an un-pinned `vmin` is floored at the smallest positive
    value that is not an extreme low outlier (`ArrayGlyph._log_safe_vmin`),
    so a lone near-zero pixel does not drag the bar's decades below the
    data's bulk (issue #339); pass an explicit `vmin` to keep the raw
    minimum.

    Examples:
        - The log scale exposes no extra knobs:
            ```python
            >>> from cleopatra.styling.scaling import ColorScaling
            >>> ColorScaling.log().kind.value
            'lognorm'

            ```
    """
    return cls(kind=ColorScale.LOGNORM)

midpoint(at=0) classmethod #

A midpoint-anchored diverging colour scale.

Parameters:

Name Type Description Default
at float

The value pinned to the colormap centre. Defaults to 0.

0

Examples:

  • Anchor the colormap centre at a chosen value:
    >>> from cleopatra.styling.scaling import ColorScaling
    >>> ColorScaling.midpoint(at=100).center
    100
    
Source code in src/cleopatra/styling/scaling.py
@classmethod
def midpoint(cls, at: float = 0) -> ColorScaling:
    """A midpoint-anchored diverging colour scale.

    Args:
        at: The value pinned to the colormap centre. Defaults to `0`.

    Examples:
        - Anchor the colormap centre at a chosen value:
            ```python
            >>> from cleopatra.styling.scaling import ColorScaling
            >>> ColorScaling.midpoint(at=100).center
            100

            ```
    """
    return cls(kind=ColorScale.MIDPOINT, center=at)

power(gamma=0.5) classmethod #

A power-law (PowerNorm) colour scale.

Parameters:

Name Type Description Default
gamma float

The power exponent. Defaults to 0.5.

0.5

Examples:

  • Only gamma is exposed:
    >>> from cleopatra.styling.scaling import ColorScaling
    >>> ColorScaling.power(gamma=2.0).gamma
    2.0
    
Source code in src/cleopatra/styling/scaling.py
@classmethod
def power(cls, gamma: float = 0.5) -> ColorScaling:
    """A power-law (`PowerNorm`) colour scale.

    Args:
        gamma: The power exponent. Defaults to `0.5`.

    Examples:
        - Only `gamma` is exposed:
            ```python
            >>> from cleopatra.styling.scaling import ColorScaling
            >>> ColorScaling.power(gamma=2.0).gamma
            2.0

            ```
    """
    return cls(kind=ColorScale.POWER, gamma=gamma)

sym_log(threshold=None, scale=None) classmethod #

A symmetric-log (SymLogNorm) colour scale.

Parameters:

Name Type Description Default
threshold float | None

The linear-region half-width (linthresh) -- the boundary between the linear band around zero and the log tail. Defaults to None, which auto-derives it from the data range at render time (a small fraction of the data's peak magnitude), so the log decades track the data's own scale instead of running far below it. Pass an explicit value to pin the band near a scale you care about; an explicit threshold always wins over the auto-derivation.

None
scale float | None

The linear-region scale factor (linscale) -- how much colour-bar width the linear band around zero occupies. Defaults to None, which pairs a sensible width (matplotlib's 1.0) with the auto-derived threshold so the widened linear band stays legible. Pass an explicit value to override it; an explicit scale always wins.

None

Examples:

  • Exposes the two sym-lognorm knobs:
    >>> from cleopatra.styling.scaling import ColorScaling
    >>> s = ColorScaling.sym_log(threshold=0.01, scale=0.1)
    >>> (s.line_threshold, s.line_scale)
    (0.01, 0.1)
    
  • The default defers the threshold to the data range:
    >>> ColorScaling.sym_log().line_threshold is None
    True
    
Source code in src/cleopatra/styling/scaling.py
@classmethod
def sym_log(
    cls, threshold: float | None = None, scale: float | None = None
) -> ColorScaling:
    """A symmetric-log (`SymLogNorm`) colour scale.

    Args:
        threshold: The linear-region half-width (`linthresh`) -- the
            boundary between the linear band around zero and the log tail.
            Defaults to `None`, which auto-derives it from the data range at
            render time (a small fraction of the data's peak magnitude), so
            the log decades track the data's own scale instead of running
            far below it. Pass an explicit value to pin the band near a
            scale you care about; an explicit `threshold` always wins over
            the auto-derivation.
        scale: The linear-region scale factor (`linscale`) -- how much
            colour-bar width the linear band around zero occupies. Defaults
            to `None`, which pairs a sensible width (matplotlib's `1.0`)
            with the auto-derived `threshold` so the widened linear band
            stays legible. Pass an explicit value to override it; an
            explicit `scale` always wins.

    Examples:
        - Exposes the two `sym-lognorm` knobs:
            ```python
            >>> from cleopatra.styling.scaling import ColorScaling
            >>> s = ColorScaling.sym_log(threshold=0.01, scale=0.1)
            >>> (s.line_threshold, s.line_scale)
            (0.01, 0.1)

            ```
        - The default defers the threshold to the data range:
            ```python
            >>> ColorScaling.sym_log().line_threshold is None
            True

            ```
    """
    return cls(kind=ColorScale.SYM_LOGNORM, line_threshold=threshold, line_scale=scale)

to_options() #

Flatten back to the default_options keys the engine reads.

Returns:

Name Type Description
dict dict[str, Any]

The colour-scale keys, with color_scale as the plain string value and norm reset to None (a scale clears any raw-norm escape hatch).

Examples:

  • Emits the flat keys a glyph merges into default_options:
    >>> from cleopatra.styling.scaling import ColorScaling
    >>> ColorScaling.power(gamma=0.7).to_options()["color_scale"]
    'power'
    
Source code in src/cleopatra/styling/scaling.py
def to_options(self) -> dict[str, Any]:
    """Flatten back to the `default_options` keys the engine reads.

    Returns:
        dict: The colour-scale keys, with `color_scale` as the plain
            string value and `norm` reset to `None` (a scale clears any
            raw-norm escape hatch).

    Examples:
        - Emits the flat keys a glyph merges into `default_options`:
            ```python
            >>> from cleopatra.styling.scaling import ColorScaling
            >>> ColorScaling.power(gamma=0.7).to_options()["color_scale"]
            'power'

            ```
    """
    return {
        "color_scale": self.kind.value,
        "gamma": self.gamma,
        "line_threshold": self.line_threshold,
        "line_scale": self.line_scale,
        "bounds": self.bounds,
        "midpoint": self.center,
        "samples": self.samples,
        # A scale is a full reset: choosing one clears any raw-norm escape
        # hatch (`plot(norm=...)`) so a later `color=ColorScaling.*` is not
        # silently shadowed by a sticky caller norm.
        "norm": None,
    }

Contour#

Discrete colour levels and inline contour labels: plot(contour=Contour(levels=6, labels=True)). Also a hatch encoding — a pattern per band for marking a region without spending the colour channel; fill=False draws the hatching alone, the significance/uncertainty overlay form: plot(kind="contourf", contour=Contour(levels=[0.5, 1.5], hatches=["///"], fill=False, hatch_color="0.2")).

cleopatra.styling.params.Contour dataclass #

Contour discretisation and inline-label options.

Groups the levels / labels / label_kw options plus the hatch encoding (hatches / fill / hatch_color). levels applies to every colour-mapped glyph that discretises a scale (array, vector, flow, polygon, scatter, kde); labels / label_kw draw inline numeric labels on isolines and are honoured only by the glyphs that render contour lines (ArrayGlyph with kind="contour", MeshGlyph node contours). The hatch fields draw a pattern per band on the contourf path (ArrayGlyph today), so a mask can be marked without spending the colour channel -- fill=False leaves only the hatching, the significance/uncertainty overlay form.

Attributes:

Name Type Description
levels int | Sequence[float] | None

Discrete colour levels -- an int count or an explicit sequence of edges. None leaves the scale continuous.

labels bool | None

Draw inline numeric labels on isolines. None leaves the glyph default (False).

label_kw dict[str, Any] | None

Extra keyword arguments forwarded to ax.clabel when labels is true.

hatches Sequence[str | None] | None

A hatch pattern per band, e.g. ["", "///"] or ["...", None] -- one entry per interval between levels (matplotlib cycles a short list). None draws no hatching. Honoured on the contourf render path.

fill bool | None

False renders the bands unfilled (colors="none"), so only the hatch marks draw -- the overlay form used for a significance or uncertainty mask. None keeps the default filled behaviour.

hatch_color str | None

Colour of the hatch strokes for this set only, applied via QuadContourSet.set_hatchcolor (matplotlib >= 3.11) so it recolours the hatching independently of the global hatch.color rcParam and without touching the band edges. None leaves matplotlib's default.

Examples:

  • Only the set fields are emitted:
    >>> from cleopatra.styling.params import Contour
    >>> Contour(levels=5).to_options()
    {'levels': 5}
    >>> Contour(labels=True, label_kw={"fontsize": 8}).to_options()
    {'labels': True, 'label_kw': {'fontsize': 8}}
    
  • A hatch overlay emits its own keys and nothing else:
    >>> from cleopatra.styling.params import Contour
    >>> Contour(hatches=["///"], fill=False, hatch_color="0.2").to_options()
    {'hatches': ['///'], 'fill': False, 'hatch_color': '0.2'}
    
Source code in src/cleopatra/styling/params.py
@dataclass(frozen=True)
class Contour:
    """Contour discretisation and inline-label options.

    Groups the `levels` / `labels` / `label_kw` options plus the hatch
    encoding (`hatches` / `fill` / `hatch_color`). `levels` applies to every
    colour-mapped glyph that discretises a scale (array, vector, flow,
    polygon, scatter, kde); `labels` / `label_kw` draw inline numeric labels
    on isolines and are honoured only by the glyphs that render contour lines
    (`ArrayGlyph` with `kind="contour"`, `MeshGlyph` node contours). The hatch
    fields draw a *pattern* per band on the `contourf` path (`ArrayGlyph`
    today), so a mask can be marked without spending the colour channel --
    `fill=False` leaves only the hatching, the significance/uncertainty
    overlay form.

    Attributes:
        levels: Discrete colour levels -- an int count or an explicit
            sequence of edges. `None` leaves the scale continuous.
        labels: Draw inline numeric labels on isolines. `None` leaves the
            glyph default (`False`).
        label_kw: Extra keyword arguments forwarded to `ax.clabel` when
            `labels` is true.
        hatches: A hatch pattern per band, e.g. `["", "///"]` or
            `["...", None]` -- one entry per interval between `levels`
            (matplotlib cycles a short list). `None` draws no hatching.
            Honoured on the `contourf` render path.
        fill: `False` renders the bands unfilled (`colors="none"`), so only
            the hatch marks draw -- the overlay form used for a significance
            or uncertainty mask. `None` keeps the default filled behaviour.
        hatch_color: Colour of the hatch strokes for this set only, applied
            via `QuadContourSet.set_hatchcolor` (matplotlib >= 3.11) so it
            recolours the hatching independently of the global `hatch.color`
            rcParam and without touching the band edges. `None` leaves
            matplotlib's default.

    Examples:
        - Only the set fields are emitted:
            ```python
            >>> from cleopatra.styling.params import Contour
            >>> Contour(levels=5).to_options()
            {'levels': 5}
            >>> Contour(labels=True, label_kw={"fontsize": 8}).to_options()
            {'labels': True, 'label_kw': {'fontsize': 8}}

            ```
        - A hatch overlay emits its own keys and nothing else:
            ```python
            >>> from cleopatra.styling.params import Contour
            >>> Contour(hatches=["///"], fill=False, hatch_color="0.2").to_options()
            {'hatches': ['///'], 'fill': False, 'hatch_color': '0.2'}

            ```
    """

    levels: int | Sequence[float] | None = None
    labels: bool | None = None
    label_kw: dict[str, Any] | None = None
    hatches: Sequence[str | None] | None = None
    fill: bool | None = None
    hatch_color: str | None = None

    def to_options(self) -> dict[str, Any]:
        """Flatten the explicitly-set fields into `default_options` keys.

        Returns:
            dict: `levels` / `labels` / `label_kw` / `hatches` / `fill` /
                `hatch_color` for the fields that were set (non-`None`); an
                empty dict when nothing was set.
        """
        options: dict[str, Any] = {}
        if self.levels is not None:
            options["levels"] = self.levels
        if self.labels is not None:
            options["labels"] = self.labels
        if self.label_kw is not None:
            options["label_kw"] = self.label_kw
        if self.hatches is not None:
            options["hatches"] = list(self.hatches)
        if self.fill is not None:
            options["fill"] = self.fill
        if self.hatch_color is not None:
            options["hatch_color"] = self.hatch_color
        return options

to_options() #

Flatten the explicitly-set fields into default_options keys.

Returns:

Name Type Description
dict dict[str, Any]

levels / labels / label_kw / hatches / fill / hatch_color for the fields that were set (non-None); an empty dict when nothing was set.

Source code in src/cleopatra/styling/params.py
def to_options(self) -> dict[str, Any]:
    """Flatten the explicitly-set fields into `default_options` keys.

    Returns:
        dict: `levels` / `labels` / `label_kw` / `hatches` / `fill` /
            `hatch_color` for the fields that were set (non-`None`); an
            empty dict when nothing was set.
    """
    options: dict[str, Any] = {}
    if self.levels is not None:
        options["levels"] = self.levels
    if self.labels is not None:
        options["labels"] = self.labels
    if self.label_kw is not None:
        options["label_kw"] = self.label_kw
    if self.hatches is not None:
        options["hatches"] = list(self.hatches)
    if self.fill is not None:
        options["fill"] = self.fill
    if self.hatch_color is not None:
        options["hatch_color"] = self.hatch_color
    return options

CellValues#

Per-cell value-text overlay (ArrayGlyph): plot(cells=CellValues(show=True, size=10)).

cleopatra.styling.params.CellValues dataclass #

Per-cell value-text display options (ArrayGlyph only).

Groups the display_cell_value / num_size / background_color_threshold options that overlay each cell's numeric value on an imshow / pcolormesh render.

Attributes:

Name Type Description
show bool | None

Draw each cell's value as text. None leaves the glyph default (False).

size int | None

Font size of the cell-value text. None leaves the default.

background_threshold float | None

Value above which the text switches to the light colour (for contrast against a dark cell). None leaves the default (max(array) / 2).

Examples:

  • Enable the overlay with a custom font size:
    >>> from cleopatra.styling.params import CellValues
    >>> CellValues(show=True, size=10).to_options()
    {'display_cell_value': True, 'num_size': 10}
    
Source code in src/cleopatra/styling/params.py
@dataclass(frozen=True)
class CellValues:
    """Per-cell value-text display options (`ArrayGlyph` only).

    Groups the `display_cell_value` / `num_size` /
    `background_color_threshold` options that overlay each cell's numeric
    value on an `imshow` / `pcolormesh` render.

    Attributes:
        show: Draw each cell's value as text. `None` leaves the glyph
            default (`False`).
        size: Font size of the cell-value text. `None` leaves the default.
        background_threshold: Value above which the text switches to the
            light colour (for contrast against a dark cell). `None` leaves
            the default (`max(array) / 2`).

    Examples:
        - Enable the overlay with a custom font size:
            ```python
            >>> from cleopatra.styling.params import CellValues
            >>> CellValues(show=True, size=10).to_options()
            {'display_cell_value': True, 'num_size': 10}

            ```
    """

    show: bool | None = None
    size: int | None = None
    background_threshold: float | None = None

    def to_options(self) -> dict[str, Any]:
        """Flatten the explicitly-set fields into `default_options` keys.

        Returns:
            dict: `display_cell_value` / `num_size` /
                `background_color_threshold` for the fields that were set.
        """
        options: dict[str, Any] = {}
        if self.show is not None:
            options["display_cell_value"] = self.show
        if self.size is not None:
            options["num_size"] = self.size
        if self.background_threshold is not None:
            options["background_color_threshold"] = self.background_threshold
        return options

to_options() #

Flatten the explicitly-set fields into default_options keys.

Returns:

Name Type Description
dict dict[str, Any]

display_cell_value / num_size / background_color_threshold for the fields that were set.

Source code in src/cleopatra/styling/params.py
def to_options(self) -> dict[str, Any]:
    """Flatten the explicitly-set fields into `default_options` keys.

    Returns:
        dict: `display_cell_value` / `num_size` /
            `background_color_threshold` for the fields that were set.
    """
    options: dict[str, Any] = {}
    if self.show is not None:
        options["display_cell_value"] = self.show
    if self.size is not None:
        options["num_size"] = self.size
    if self.background_threshold is not None:
        options["background_color_threshold"] = self.background_threshold
    return options

DataStyle#

Named preset, relief shading, and per-call preset overrides: plot(data_style=DataStyle(style="topography", hillshade=True)).

cleopatra.styling.params.DataStyle dataclass #

Named data-style preset, relief-shading, and per-call preset overrides.

Groups the style / hillshade options honoured by ArrayGlyph, MeshGlyph, and KDEGlyph, plus the bands / alpha / alpha_range per-call overrides of an active ArrayGlyph preset. Each field has three states: left unset (keep the glyph's current value -- these options are sticky), set to a value (apply it), or set explicitly to None (clear the preset / disable hillshade / drop the override). to_options() emits a key only for a field that was given (set or explicit None), never for an unset one.

The bands / alpha / alpha_range fields override just one aspect of a styled render while keeping the rest of the preset; they are only meaningful alongside a style (they replace one field of the active DATA_STYLES preset). bands rebands the scale (replacing the preset's levels); alpha sets a constant opacity and alpha_range a value-linked one -- the two are mutually exclusive and resolved downstream (a constant alpha wins). They apply to a continuous/levelled preset only; a categorical (class-colour) preset renders opaque with its fixed class colours and ignores these overrides.

Attributes:

Name Type Description
style str | None | _Unset

Name of a cleopatra.styling.colors.DATA_STYLES preset, or None to clear a sticky preset back to plain colouring.

hillshade bool | dict[str, Any] | None | _Unset

Relief-shade a regular-grid DEM -- True for defaults, a dict tuning vert_exag / azimuth / altitude / blend_mode / multidirectional, or None/False to disable.

bands int | None | _Unset

Discrete band count partitioning the preset's value range, replacing the preset's own levels/bands. Rebands a plain linear scale only -- it is ignored (with a warning) on a diverging (center) or log/symlog preset, whose own scale is kept. None clears a sticky override, keeping the preset's own scale.

alpha float | None | _Unset

Constant layer opacity in [0, 1] overriding the preset's opacity. None clears a sticky override.

alpha_range tuple[float, float] | None | _Unset

(vmin, vmax) mapping data values to opacity (a value-linked alpha) overriding the preset's opacity. None clears a sticky override.

Examples:

  • Select a preset and turn on relief shading:
    >>> from cleopatra.styling.params import DataStyle
    >>> DataStyle(style="dem", hillshade=True).to_options()
    {'style': 'dem', 'hillshade': True}
    
  • Override a styled preset's banding and opacity per call:
    >>> from cleopatra.styling.params import DataStyle
    >>> DataStyle(style="temperature_2m", bands=6, alpha=0.5).to_options()
    {'style': 'temperature_2m', 'bands': 6, 'alpha': 0.5, 'alpha_range': None}
    >>> DataStyle(alpha_range=(0.0, 40.0)).to_options()
    {'alpha_range': (0.0, 40.0), 'alpha': None}
    
  • An unset field is omitted (keeping the sticky value); an explicit None is emitted (clearing it):
    >>> from cleopatra.styling.params import DataStyle
    >>> DataStyle().to_options()
    {}
    >>> DataStyle(style=None).to_options()
    {'style': None}
    
Source code in src/cleopatra/styling/params.py
@dataclass(frozen=True)
class DataStyle:
    """Named data-style preset, relief-shading, and per-call preset overrides.

    Groups the `style` / `hillshade` options honoured by `ArrayGlyph`,
    `MeshGlyph`, and `KDEGlyph`, plus the `bands` / `alpha` / `alpha_range`
    per-call overrides of an active `ArrayGlyph` preset. Each field has three
    states: left unset (keep the glyph's current value -- these options are
    sticky), set to a value (apply it), or set explicitly to `None` (clear the
    preset / disable hillshade / drop the override). `to_options()` emits a key
    only for a field that was given (set or explicit `None`), never for an
    unset one.

    The `bands` / `alpha` / `alpha_range` fields override just one aspect of a
    styled render while keeping the rest of the preset; they are only
    meaningful alongside a `style` (they replace one field of the active
    `DATA_STYLES` preset). `bands` rebands the scale (replacing the preset's
    `levels`); `alpha` sets a constant opacity and `alpha_range` a value-linked
    one -- the two are mutually exclusive and resolved downstream (a constant
    `alpha` wins). They apply to a continuous/levelled preset only; a
    categorical (class-colour) preset renders opaque with its fixed class
    colours and ignores these overrides.

    Attributes:
        style: Name of a `cleopatra.styling.colors.DATA_STYLES` preset, or
            `None` to clear a sticky preset back to plain colouring.
        hillshade: Relief-shade a regular-grid DEM -- `True` for defaults,
            a dict tuning `vert_exag` / `azimuth` / `altitude` /
            `blend_mode` / `multidirectional`, or `None`/`False` to
            disable.
        bands: Discrete band count partitioning the preset's value range,
            replacing the preset's own `levels`/`bands`. Rebands a plain
            linear scale only -- it is ignored (with a warning) on a diverging
            (`center`) or `log`/`symlog` preset, whose own scale is kept.
            `None` clears a sticky override, keeping the preset's own scale.
        alpha: Constant layer opacity in `[0, 1]` overriding the preset's
            opacity. `None` clears a sticky override.
        alpha_range: `(vmin, vmax)` mapping data values to opacity (a
            value-linked alpha) overriding the preset's opacity. `None`
            clears a sticky override.

    Examples:
        - Select a preset and turn on relief shading:
            ```python
            >>> from cleopatra.styling.params import DataStyle
            >>> DataStyle(style="dem", hillshade=True).to_options()
            {'style': 'dem', 'hillshade': True}

            ```
        - Override a styled preset's banding and opacity per call:
            ```python
            >>> from cleopatra.styling.params import DataStyle
            >>> DataStyle(style="temperature_2m", bands=6, alpha=0.5).to_options()
            {'style': 'temperature_2m', 'bands': 6, 'alpha': 0.5, 'alpha_range': None}
            >>> DataStyle(alpha_range=(0.0, 40.0)).to_options()
            {'alpha_range': (0.0, 40.0), 'alpha': None}

            ```
        - An unset field is omitted (keeping the sticky value); an
            explicit `None` is emitted (clearing it):
            ```python
            >>> from cleopatra.styling.params import DataStyle
            >>> DataStyle().to_options()
            {}
            >>> DataStyle(style=None).to_options()
            {'style': None}

            ```
    """

    style: str | None | _Unset = _UNSET
    hillshade: bool | dict[str, Any] | None | _Unset = _UNSET
    bands: int | None | _Unset = _UNSET
    alpha: float | None | _Unset = _UNSET
    alpha_range: tuple[float, float] | None | _Unset = _UNSET

    def __post_init__(self) -> None:
        """Validate `alpha_range` is a `(vmin, vmax)` numeric pair when given.

        Raises:
            TypeError: If `alpha_range` is set to something that is not a
                length-2 sequence of numbers, so the error surfaces at the
                `DataStyle` boundary rather than deep inside the render.
        """
        ar = self.alpha_range
        if isinstance(ar, _Unset) or ar is None:
            return
        try:
            lo, hi = ar
            float(lo), float(hi)
        except (TypeError, ValueError) as exc:
            raise TypeError(
                "DataStyle(alpha_range=...) must be a (vmin, vmax) pair of "
                f"numbers, got {ar!r}"
            ) from exc

    @classmethod
    def for_apply_style(
        cls,
        style: str | None,
        hillshade: bool | dict[str, Any] | None | _Unset = _UNSET,
    ) -> DataStyle:
        """Build the `DataStyle` an `apply_style(...)` call forwards to `plot`.

        Folds a preset `style` and an optionally-forwarded `hillshade` into one
        object: when `hillshade` is left unset (the default sentinel) it is
        omitted so any sticky relief shading is kept; an explicit value (a dict,
        `True`/`False`, or `None` to clear) flows through to
        `DataStyle(hillshade=...)`. Centralises the sentinel-gated construction
        that the `apply_style` helpers of `ArrayGlyph`, `MeshGlyph`, and
        `KDEGlyph` previously each hand-rolled with their own sentinels.

        Args:
            style: The `DATA_STYLES` preset name to apply (or `None` to clear).
            hillshade: Relief-shading override, or the `_UNSET` sentinel
                (default) to leave it unset.

        Returns:
            DataStyle: `DataStyle(style=style)` when `hillshade` is unset, else
                `DataStyle(style=style, hillshade=hillshade)`.

        Examples:
            ```python
            >>> from cleopatra.styling.params import DataStyle
            >>> DataStyle.for_apply_style("dem").to_options()
            {'style': 'dem'}
            >>> DataStyle.for_apply_style("dem", hillshade=True).to_options()
            {'style': 'dem', 'hillshade': True}

            ```
        """
        if isinstance(hillshade, _Unset):
            return cls(style=style)
        return cls(style=style, hillshade=hillshade)

    def to_options(self) -> dict[str, Any]:
        """Flatten the explicitly-given fields into `default_options` keys.

        Returns:
            dict: `style` / `hillshade` / `bands` / `alpha` / `alpha_range`
                for the fields the caller gave (a value or an explicit
                `None`); unset fields are omitted. Setting one of the two
                mutually-exclusive opacity fields to a real value also emits an
                explicit `None` for the other, so a mode switch clears the
                sticky opposite field.
        """
        options: dict[str, Any] = {}
        if not isinstance(self.style, _Unset):
            options["style"] = self.style
        if not isinstance(self.hillshade, _Unset):
            options["hillshade"] = self.hillshade
        if not isinstance(self.bands, _Unset):
            options["bands"] = self.bands
        alpha_set = not isinstance(self.alpha, _Unset)
        range_set = not isinstance(self.alpha_range, _Unset)
        if alpha_set:
            options["alpha"] = self.alpha
        if range_set:
            options["alpha_range"] = self.alpha_range
        # `alpha` (constant) and `alpha_range` (value-linked) are mutually
        # exclusive opacity modes. Setting one to a real value emits an explicit
        # `None` for the other so switching modes on the same (sticky) glyph is
        # not defeated by the stale field -- a leftover constant `alpha` would
        # otherwise win the tie-break in `resolve_style_overrides`. Clearing a
        # field (`=None`) leaves the other untouched.
        if alpha_set and self.alpha is not None and not range_set:
            options["alpha_range"] = None
        elif range_set and self.alpha_range is not None and not alpha_set:
            options["alpha"] = None
        return options

__post_init__() #

Validate alpha_range is a (vmin, vmax) numeric pair when given.

Raises:

Type Description
TypeError

If alpha_range is set to something that is not a length-2 sequence of numbers, so the error surfaces at the DataStyle boundary rather than deep inside the render.

Source code in src/cleopatra/styling/params.py
def __post_init__(self) -> None:
    """Validate `alpha_range` is a `(vmin, vmax)` numeric pair when given.

    Raises:
        TypeError: If `alpha_range` is set to something that is not a
            length-2 sequence of numbers, so the error surfaces at the
            `DataStyle` boundary rather than deep inside the render.
    """
    ar = self.alpha_range
    if isinstance(ar, _Unset) or ar is None:
        return
    try:
        lo, hi = ar
        float(lo), float(hi)
    except (TypeError, ValueError) as exc:
        raise TypeError(
            "DataStyle(alpha_range=...) must be a (vmin, vmax) pair of "
            f"numbers, got {ar!r}"
        ) from exc

for_apply_style(style, hillshade=_UNSET) classmethod #

Build the DataStyle an apply_style(...) call forwards to plot.

Folds a preset style and an optionally-forwarded hillshade into one object: when hillshade is left unset (the default sentinel) it is omitted so any sticky relief shading is kept; an explicit value (a dict, True/False, or None to clear) flows through to DataStyle(hillshade=...). Centralises the sentinel-gated construction that the apply_style helpers of ArrayGlyph, MeshGlyph, and KDEGlyph previously each hand-rolled with their own sentinels.

Parameters:

Name Type Description Default
style str | None

The DATA_STYLES preset name to apply (or None to clear).

required
hillshade bool | dict[str, Any] | None | _Unset

Relief-shading override, or the _UNSET sentinel (default) to leave it unset.

_UNSET

Returns:

Name Type Description
DataStyle DataStyle

DataStyle(style=style) when hillshade is unset, else DataStyle(style=style, hillshade=hillshade).

Examples:

>>> from cleopatra.styling.params import DataStyle
>>> DataStyle.for_apply_style("dem").to_options()
{'style': 'dem'}
>>> DataStyle.for_apply_style("dem", hillshade=True).to_options()
{'style': 'dem', 'hillshade': True}
Source code in src/cleopatra/styling/params.py
@classmethod
def for_apply_style(
    cls,
    style: str | None,
    hillshade: bool | dict[str, Any] | None | _Unset = _UNSET,
) -> DataStyle:
    """Build the `DataStyle` an `apply_style(...)` call forwards to `plot`.

    Folds a preset `style` and an optionally-forwarded `hillshade` into one
    object: when `hillshade` is left unset (the default sentinel) it is
    omitted so any sticky relief shading is kept; an explicit value (a dict,
    `True`/`False`, or `None` to clear) flows through to
    `DataStyle(hillshade=...)`. Centralises the sentinel-gated construction
    that the `apply_style` helpers of `ArrayGlyph`, `MeshGlyph`, and
    `KDEGlyph` previously each hand-rolled with their own sentinels.

    Args:
        style: The `DATA_STYLES` preset name to apply (or `None` to clear).
        hillshade: Relief-shading override, or the `_UNSET` sentinel
            (default) to leave it unset.

    Returns:
        DataStyle: `DataStyle(style=style)` when `hillshade` is unset, else
            `DataStyle(style=style, hillshade=hillshade)`.

    Examples:
        ```python
        >>> from cleopatra.styling.params import DataStyle
        >>> DataStyle.for_apply_style("dem").to_options()
        {'style': 'dem'}
        >>> DataStyle.for_apply_style("dem", hillshade=True).to_options()
        {'style': 'dem', 'hillshade': True}

        ```
    """
    if isinstance(hillshade, _Unset):
        return cls(style=style)
    return cls(style=style, hillshade=hillshade)

to_options() #

Flatten the explicitly-given fields into default_options keys.

Returns:

Name Type Description
dict dict[str, Any]

style / hillshade / bands / alpha / alpha_range for the fields the caller gave (a value or an explicit None); unset fields are omitted. Setting one of the two mutually-exclusive opacity fields to a real value also emits an explicit None for the other, so a mode switch clears the sticky opposite field.

Source code in src/cleopatra/styling/params.py
def to_options(self) -> dict[str, Any]:
    """Flatten the explicitly-given fields into `default_options` keys.

    Returns:
        dict: `style` / `hillshade` / `bands` / `alpha` / `alpha_range`
            for the fields the caller gave (a value or an explicit
            `None`); unset fields are omitted. Setting one of the two
            mutually-exclusive opacity fields to a real value also emits an
            explicit `None` for the other, so a mode switch clears the
            sticky opposite field.
    """
    options: dict[str, Any] = {}
    if not isinstance(self.style, _Unset):
        options["style"] = self.style
    if not isinstance(self.hillshade, _Unset):
        options["hillshade"] = self.hillshade
    if not isinstance(self.bands, _Unset):
        options["bands"] = self.bands
    alpha_set = not isinstance(self.alpha, _Unset)
    range_set = not isinstance(self.alpha_range, _Unset)
    if alpha_set:
        options["alpha"] = self.alpha
    if range_set:
        options["alpha_range"] = self.alpha_range
    # `alpha` (constant) and `alpha_range` (value-linked) are mutually
    # exclusive opacity modes. Setting one to a real value emits an explicit
    # `None` for the other so switching modes on the same (sticky) glyph is
    # not defeated by the stale field -- a leftover constant `alpha` would
    # otherwise win the tie-break in `resolve_style_overrides`. Clearing a
    # field (`=None`) leaves the other untouched.
    if alpha_set and self.alpha is not None and not range_set:
        options["alpha_range"] = None
    elif range_set and self.alpha_range is not None and not alpha_set:
        options["alpha"] = None
    return options

Classify#

Classed colour schemes on the scatter / vector / flow / polygon glyphs and on the raster ArrayGlyph (plot / facet / animate): plot(classify=Classify(scheme="quantiles", k=5)). ArrayGlyph bins its 2-D field into the same discrete classes with a stepped colorbar — a named scheme, or explicit edges Classify(scheme=[0, 10, 50, 100, 500]) — resolving the classes once over the whole stack when faceting / animating. Its scheme="categorical" is rejected (a raster's cells are a continuous field, not nominal labels).

cleopatra.styling.params.Classify dataclass #

Value-classification (choropleth) options.

Groups the scheme / k / category_legend_kwargs options honoured by the glyphs whose colour mapping routes through Glyph._prepare_scalar_mapping -- VectorGlyph, FlowGlyph, PolygonGlyph, ScatterGlyph -- and by ArrayGlyph, which bins its 2-D field into the same discrete colour classes on plot / facet / animate (its scheme="categorical" is rejected, since a raster's cells are a continuous field rather than nominal labels).

Attributes:

Name Type Description
scheme str | Sequence[float] | None

A cleopatra.styling.styles.classify scheme name (e.g. "quantiles", "equal_interval"), an explicit sequence of bin edges, or the literal "categorical" for a distinct-value mapping. None leaves the default (no classification).

k int | None

The class count for count/width schemes. None leaves the default (5).

category_legend_kwargs dict[str, Any] | None

Extra keyword arguments forwarded to the legend a "categorical" scheme draws (e.g. loc, ncol).

Examples:

  • A quantile scheme with four classes:
    >>> from cleopatra.styling.params import Classify
    >>> Classify(scheme="quantiles", k=4).to_options()
    {'scheme': 'quantiles', 'k': 4}
    
Source code in src/cleopatra/styling/params.py
@dataclass(frozen=True)
class Classify:
    """Value-classification (choropleth) options.

    Groups the `scheme` / `k` / `category_legend_kwargs` options honoured
    by the glyphs whose colour mapping routes through
    `Glyph._prepare_scalar_mapping` -- `VectorGlyph`, `FlowGlyph`,
    `PolygonGlyph`, `ScatterGlyph` -- and by `ArrayGlyph`, which bins its
    2-D field into the same discrete colour classes on `plot` / `facet` /
    `animate` (its `scheme="categorical"` is rejected, since a raster's cells
    are a continuous field rather than nominal labels).

    Attributes:
        scheme: A `cleopatra.styling.styles.classify` scheme name (e.g.
            `"quantiles"`, `"equal_interval"`), an explicit sequence of bin
            edges, or the literal `"categorical"` for a distinct-value
            mapping. `None` leaves the default (no classification).
        k: The class count for count/width schemes. `None` leaves the
            default (`5`).
        category_legend_kwargs: Extra keyword arguments forwarded to the
            legend a `"categorical"` scheme draws (e.g. `loc`, `ncol`).

    Examples:
        - A quantile scheme with four classes:
            ```python
            >>> from cleopatra.styling.params import Classify
            >>> Classify(scheme="quantiles", k=4).to_options()
            {'scheme': 'quantiles', 'k': 4}

            ```
    """

    scheme: str | Sequence[float] | None = None
    k: int | None = None
    category_legend_kwargs: dict[str, Any] | None = None

    def to_options(self) -> dict[str, Any]:
        """Flatten the explicitly-set fields into `default_options` keys.

        Returns:
            dict: `scheme` / `k` / `category_legend_kwargs` for the fields
                that were set.
        """
        options: dict[str, Any] = {}
        if self.scheme is not None:
            options["scheme"] = self.scheme
        if self.k is not None:
            options["k"] = self.k
        if self.category_legend_kwargs is not None:
            options["category_legend_kwargs"] = self.category_legend_kwargs
        return options

to_options() #

Flatten the explicitly-set fields into default_options keys.

Returns:

Name Type Description
dict dict[str, Any]

scheme / k / category_legend_kwargs for the fields that were set.

Source code in src/cleopatra/styling/params.py
def to_options(self) -> dict[str, Any]:
    """Flatten the explicitly-set fields into `default_options` keys.

    Returns:
        dict: `scheme` / `k` / `category_legend_kwargs` for the fields
            that were set.
    """
    options: dict[str, Any] = {}
    if self.scheme is not None:
        options["scheme"] = self.scheme
    if self.k is not None:
        options["k"] = self.k
    if self.category_legend_kwargs is not None:
        options["category_legend_kwargs"] = self.category_legend_kwargs
    return options

ColorBar#

Colorbar placement, caption, and sizing: plot(colorbar=ColorBar(location="bottom", label="mm/day")). Pass colorbar=True/False for the simple cases.

cleopatra.styling.colorbar.ColorBar #

Placement (and backing box) for the colorbar plot / animate draws.

Bundles the colorbar-layout choices -- which edge it sits on, whether it is inset inside the frame, and its backing box -- into one value passed as plot(colorbar=...) / animate(colorbar=...), mirroring FrameLabel. Pass colorbar=True / False / None for the simple cases and a ColorBar for placement control.

Attributes:

Name Type Description
location

Edge the colorbar sits on -- "left", "right", "top", or "bottom". None (default) keeps matplotlib's placement (right of a vertical bar). Left/right force a vertical bar, top/bottom a horizontal one.

orientation

Bar orientation -- "vertical" or "horizontal". None (default) lets location decide, and yields a vertical bar when location is None too. Because a set location fixes the orientation, an orientation that disagrees with it is ignored (with a UserWarning) -- set only one. The resolved orientation is sticky on a reused glyph: a later ColorBar() with orientation unset does not reset a previously applied one.

inside

When True, the colorbar is inset inside the frame at location (overlaying the data) rather than in an outside gutter, by default False. An inset is a child of the data axes, so it tracks the axes through full_bleed.

box

Backing panel behind the scale, so the data does not show through its labels. False draws none; True an opaque white panel; a colour string a panel of that colour; a dict of matplotlib.patches.Rectangle kwargs for full control. Defaults to None, which becomes True when inside is set (an inset over moving data almost always wants a panel) and stays off otherwise. For a real colorbar the panel backs an inside colorbar only (it is ignored when inside=False, which sits in its own gutter); for a style preset's swatch legend it backs the swatch regardless of placement, and the swatch title/values then default to a colour that contrasts with the panel (an explicit label_color/tick_color still wins).

label_color

Colour of the scale's title text -- the colorbar's axis label and, for a style preset, the swatch legend's title (the endpoint values take tick_color, not this). None (default) keeps the default: matplotlib's for a colorbar label; for the swatch, a colour that contrasts with box, else white.

tick_color

Colour of the tick labels (the numbers) of a real colorbar and, for a style preset, the swatch legend's endpoint values. None (default) keeps matplotlib's default for a colorbar; for the swatch it defaults to a colour that contrasts with box, else white.

label

Caption text for the scale (the colorbar's title). None (default) keeps the current default caption.

length

Bar length as a fraction of the axis (e.g. 0.8). None (default) keeps the default length.

label_size

Font size of the caption. None (default) keeps the default.

label_rotation

Rotation of the caption in degrees. None (default) leaves matplotlib's own label orientation; pass a value to rotate the caption (e.g. 0 for a horizontal caption).

label_location

Where the caption sits along the bar (e.g. "center"). Distinct from location, which is the bar's edge. Valid values depend on orientation (vertical bar: "top"/"center"/"bottom"; horizontal bar: "left"/"center"/"right"). None (default) keeps the default.

ticks_spacing

Spacing between the colorbar's ticks. None (default) keeps the default.

Examples:

  • An inside colorbar on the right -- its box defaults on:
    >>> from cleopatra.styling.colorbar import ColorBar
    >>> spec = ColorBar(location="right", inside=True)
    >>> spec.inside, spec.box
    (True, True)
    
  • Black title + tick numbers, outside on the bottom (no box):
    >>> from cleopatra.styling.colorbar import ColorBar
    >>> spec = ColorBar(location="bottom", label_color="black", tick_color="black")
    >>> (spec.box, spec.label_color, spec.tick_color)
    (None, 'black', 'black')
    
  • A captioned bar, fully specified through the spec (no loose kwargs):
    >>> from cleopatra.styling.colorbar import ColorBar
    >>> spec = ColorBar(location="bottom", label="Rainfall mm/day", length=0.8)
    >>> (spec.label, spec.length)
    ('Rainfall mm/day', 0.8)
    
Source code in src/cleopatra/styling/colorbar.py
class ColorBar:
    """Placement (and backing box) for the colorbar `plot` / `animate` draws.

    Bundles the colorbar-layout choices -- which edge it sits on, whether it
    is inset *inside* the frame, and its backing box -- into one value passed
    as `plot(colorbar=...)` / `animate(colorbar=...)`, mirroring `FrameLabel`.
    Pass `colorbar=True` / `False` / `None` for the simple cases and a
    `ColorBar` for placement control.

    Attributes:
        location: Edge the colorbar sits on -- `"left"`, `"right"`, `"top"`,
            or `"bottom"`. `None` (default) keeps matplotlib's placement
            (right of a vertical bar). Left/right force a vertical bar,
            top/bottom a horizontal one.
        orientation: Bar orientation -- `"vertical"` or `"horizontal"`. `None`
            (default) lets `location` decide, and yields a vertical bar when
            `location` is `None` too. Because a set `location` fixes the
            orientation, an `orientation` that disagrees with it is ignored
            (with a `UserWarning`) -- set only one. The resolved orientation is
            sticky on a reused glyph: a later `ColorBar()` with `orientation`
            unset does not reset a previously applied one.
        inside: When `True`, the colorbar is inset *inside* the frame at
            `location` (overlaying the data) rather than in an outside gutter,
            by default `False`. An inset is a child of the data axes, so it
            tracks the axes through `full_bleed`.
        box: Backing panel behind the scale, so the data does not show through
            its labels. `False` draws none; `True` an opaque white panel; a
            colour string a panel of that colour; a dict of
            `matplotlib.patches.Rectangle` kwargs for full control. Defaults to
            `None`, which becomes `True` when `inside` is set (an inset over
            moving data almost always wants a panel) and stays off otherwise.
            For a real colorbar the panel backs an *inside* colorbar only (it is
            ignored when `inside=False`, which sits in its own gutter); for a
            `style` preset's swatch legend it backs the swatch regardless of
            placement, and the swatch title/values then default to a colour that
            contrasts with the panel (an explicit `label_color`/`tick_color`
            still wins).
        label_color: Colour of the scale's title text -- the colorbar's axis
            label and, for a `style` preset, the swatch legend's title (the
            endpoint values take `tick_color`, not this). `None` (default) keeps
            the default: matplotlib's for a colorbar label; for the swatch, a
            colour that contrasts with `box`, else white.
        tick_color: Colour of the tick labels (the numbers) of a real colorbar
            and, for a `style` preset, the swatch legend's endpoint values.
            `None` (default) keeps matplotlib's default for a colorbar; for the
            swatch it defaults to a colour that contrasts with `box`, else white.
        label: Caption text for the scale (the colorbar's title). `None`
            (default) keeps the current default caption.
        length: Bar length as a fraction of the axis (e.g. `0.8`). `None`
            (default) keeps the default length.
        label_size: Font size of the caption. `None` (default) keeps the
            default.
        label_rotation: Rotation of the caption in degrees. `None` (default)
            leaves matplotlib's own label orientation; pass a value to rotate
            the caption (e.g. `0` for a horizontal caption).
        label_location: Where the caption sits along the bar (e.g. `"center"`).
            Distinct from `location`, which is the bar's *edge*. Valid values
            depend on orientation (vertical bar: `"top"`/`"center"`/`"bottom"`;
            horizontal bar: `"left"`/`"center"`/`"right"`). `None` (default)
            keeps the default.
        ticks_spacing: Spacing between the colorbar's ticks. `None` (default)
            keeps the default.

    Examples:
        - An inside colorbar on the right -- its box defaults on:
            ```python
            >>> from cleopatra.styling.colorbar import ColorBar
            >>> spec = ColorBar(location="right", inside=True)
            >>> spec.inside, spec.box
            (True, True)

            ```
        - Black title + tick numbers, outside on the bottom (no box):
            ```python
            >>> from cleopatra.styling.colorbar import ColorBar
            >>> spec = ColorBar(location="bottom", label_color="black", tick_color="black")
            >>> (spec.box, spec.label_color, spec.tick_color)
            (None, 'black', 'black')

            ```
        - A captioned bar, fully specified through the spec (no loose kwargs):
            ```python
            >>> from cleopatra.styling.colorbar import ColorBar
            >>> spec = ColorBar(location="bottom", label="Rainfall mm/day", length=0.8)
            >>> (spec.label, spec.length)
            ('Rainfall mm/day', 0.8)

            ```
    """

    def __init__(
        self,
        *,
        location: Literal["left", "right", "top", "bottom"] | None = None,
        orientation: Literal["vertical", "horizontal"] | None = None,
        inside: bool = False,
        box: bool | str | dict | None = None,
        label_color: str | None = None,
        tick_color: str | None = None,
        label: str | None = None,
        length: float | None = None,
        label_size: float | None = None,
        label_rotation: float | None = None,
        label_location: str | None = None,
        ticks_spacing: float | None = None,
    ) -> None:
        """Initialise a `ColorBar`.

        Args:
            location: Edge to sit on (`"left"`/`"right"`/`"top"`/`"bottom"`),
                or `None` for matplotlib's default placement.
            orientation: Bar orientation (`"vertical"`/`"horizontal"`), or
                `None` to let `location` decide (a vertical bar when neither is
                set). Ignored (with a `UserWarning`) when it disagrees with the
                orientation `location` implies.
            inside: Inset the colorbar inside the frame, by default `False`.
            box: Backing panel for an inside colorbar (`True` / colour / dict),
                or `None` to default it on when `inside` is set.
            label_color: Colour of the scale title / colorbar label (and the
                swatch title for a `style` preset); `None` keeps the default.
            tick_color: Colour of the colorbar's tick numbers; `None` keeps
                matplotlib's default.
            label: Caption text (scale title); `None` keeps the default.
            length: Bar length as a fraction of the axis; `None` keeps the
                default.
            label_size: Caption font size; `None` keeps the default.
            label_rotation: Caption rotation in degrees; `None` leaves
                matplotlib's own label orientation.
            label_location: Caption placement along the bar (distinct from
                `location`, the bar's edge); valid values depend on orientation
                (vertical: top/center/bottom, horizontal: left/center/right);
                `None` keeps the default.
            ticks_spacing: Spacing between the colorbar's ticks; `None` keeps
                the default.
        """
        _validate_orientation(orientation)
        _warn_orientation_conflict(location, orientation)
        _validate_label_location(location, orientation, label_location)
        self.location = location
        self.orientation = orientation
        self.inside = inside
        self.box = True if (inside and box is None) else box
        self.label_color = label_color
        self.tick_color = tick_color
        self.label = label
        self.length = length
        self.label_size = label_size
        self.label_rotation = label_rotation
        self.label_location = label_location
        self.ticks_spacing = ticks_spacing

    def to_options(self) -> dict:
        """Map this spec's fields onto the `cbar_*` `default_options` keys.

        Mirrors the other grouped styling objects' `to_options`: the object
        owns the translation from its own fields to the flat render options
        `create_color_bar` reads. Placement fields are always emitted (so a
        reused glyph's prior placement is overwritten); the caption / sizing /
        orientation / tick-spacing fields are emitted only when set, leaving an
        unset field at the existing default.

        Returns:
            dict: `default_options` updates for this spec, always including
                `add_colorbar=True`.

        Examples:
            - Placement maps onto `cbar_*`; unset caption fields are omitted:
                ```python
                >>> from cleopatra.styling.colorbar import ColorBar
                >>> ColorBar(location="left", inside=True).to_options()["cbar_location"]
                'left'
                >>> "cbar_label" in ColorBar(location="right").to_options()
                False

                ```
        """
        updates = {
            "add_colorbar": True,
            "cbar_location": self.location,
            "cbar_inside": self.inside,
            "cbar_box": self.box,
            "cbar_label_color": self.label_color,
            "cbar_tick_color": self.tick_color,
        }
        optional = {
            "cbar_label": self.label,
            "cbar_length": self.length,
            "cbar_label_size": self.label_size,
            "cbar_label_rotation": self.label_rotation,
            "cbar_label_location": self.label_location,
            "cbar_orientation": self.orientation,
            "ticks_spacing": self.ticks_spacing,
        }
        updates.update({k: v for k, v in optional.items() if v is not None})
        return updates

    def specifies_placement(self) -> bool:
        """Whether this spec explicitly requests a placement or orientation.

        `True` when any of `location`, `inside`, or `orientation` is set -- the
        spec asks for a specific colorbar rather than leaving the default. Used
        to decide whether a styled (preset) render should still draw a colorbar.

        Returns:
            bool: `True` if `location`, `inside`, or `orientation` is set.

        Examples:
            - A placement edge counts as specified; a bare spec does not:
                ```python
                >>> from cleopatra.styling.colorbar import ColorBar
                >>> ColorBar(location="bottom").specifies_placement()
                True
                >>> ColorBar().specifies_placement()
                False

                ```
        """
        return (
            self.location is not None
            or self.inside
            or self.orientation is not None
        )

    @classmethod
    def reset_options(cls) -> dict:
        """`default_options` updates for a default, sticky-clearing colorbar.

        The dict `colorbar=True` applies: it draws a default bar and resets the
        resettable `cbar_*` family to `STYLE_DEFAULTS`, so a reused glyph does
        not inherit a prior sticky spec's placement or caption. Distinct from
        `to_options`, which maps a *specific* spec's fields and omits unset
        ones; this resets the whole `cbar_*` family to the defaults.
        `ticks_spacing` is deliberately excluded: it is glyph-specific
        (`KDEGlyph`, for one, auto-derives it from the data range when unset),
        so a single shared reset value could not restore each glyph's own
        default -- it is therefore left untouched by `colorbar=True`.

        Returns:
            dict: `default_options` updates for a default colorbar.

        Examples:
            - The reset always enables the bar and clears the placement:
                ```python
                >>> from cleopatra.styling.colorbar import ColorBar
                >>> opts = ColorBar.reset_options()
                >>> opts["add_colorbar"]
                True
                >>> opts["cbar_location"] is None
                True

                ```
        """
        return {
            "add_colorbar": True,
            "cbar_location": None,
            "cbar_inside": False,
            "cbar_box": None,
            "cbar_label_color": None,
            "cbar_tick_color": None,
            "cbar_orientation": STYLE_DEFAULTS["cbar_orientation"],
            "cbar_label": STYLE_DEFAULTS["cbar_label"],
            "cbar_length": STYLE_DEFAULTS["cbar_length"],
            "cbar_label_size": STYLE_DEFAULTS["cbar_label_size"],
            "cbar_label_rotation": STYLE_DEFAULTS["cbar_label_rotation"],
            "cbar_label_location": STYLE_DEFAULTS["cbar_label_location"],
        }

    @classmethod
    def resolve(cls, colorbar: "bool | ColorBar | None") -> dict:
        """Translate a `colorbar=` argument into `default_options` updates.

        Owns the full `None` / `False` / `True` / `ColorBar` dispatch: `None`
        leaves the colorbar options untouched; `False` suppresses the bar;
        `True` resets to a default bar via `reset_options`; a `ColorBar`
        instance maps its fields via `to_options`.

        Args:
            colorbar: `None`, `False`, `True`, or a `ColorBar` instance.

        Returns:
            dict: Updates to merge into `default_options` (empty for `None`).

        Raises:
            TypeError: If `colorbar` is not a bool, `ColorBar`, or `None`.

        Examples:
            ```python
            >>> from cleopatra.styling.colorbar import ColorBar
            >>> ColorBar.resolve(False)
            {'add_colorbar': False}
            >>> ColorBar.resolve(ColorBar(location="left", inside=True))["cbar_location"]
            'left'
            >>> "cbar_label" in ColorBar.resolve(ColorBar(location="right"))
            False

            ```
        """
        if colorbar is None:
            return {}
        if colorbar is False:
            return {"add_colorbar": False}
        if colorbar is True:
            return cls.reset_options()
        if isinstance(colorbar, cls):
            return colorbar.to_options()
        raise TypeError(
            f"colorbar must be a bool, ColorBar, or None, got {type(colorbar).__name__}."
        )

__init__(*, location=None, orientation=None, inside=False, box=None, label_color=None, tick_color=None, label=None, length=None, label_size=None, label_rotation=None, label_location=None, ticks_spacing=None) #

Initialise a ColorBar.

Parameters:

Name Type Description Default
location Literal['left', 'right', 'top', 'bottom'] | None

Edge to sit on ("left"/"right"/"top"/"bottom"), or None for matplotlib's default placement.

None
orientation Literal['vertical', 'horizontal'] | None

Bar orientation ("vertical"/"horizontal"), or None to let location decide (a vertical bar when neither is set). Ignored (with a UserWarning) when it disagrees with the orientation location implies.

None
inside bool

Inset the colorbar inside the frame, by default False.

False
box bool | str | dict | None

Backing panel for an inside colorbar (True / colour / dict), or None to default it on when inside is set.

None
label_color str | None

Colour of the scale title / colorbar label (and the swatch title for a style preset); None keeps the default.

None
tick_color str | None

Colour of the colorbar's tick numbers; None keeps matplotlib's default.

None
label str | None

Caption text (scale title); None keeps the default.

None
length float | None

Bar length as a fraction of the axis; None keeps the default.

None
label_size float | None

Caption font size; None keeps the default.

None
label_rotation float | None

Caption rotation in degrees; None leaves matplotlib's own label orientation.

None
label_location str | None

Caption placement along the bar (distinct from location, the bar's edge); valid values depend on orientation (vertical: top/center/bottom, horizontal: left/center/right); None keeps the default.

None
ticks_spacing float | None

Spacing between the colorbar's ticks; None keeps the default.

None
Source code in src/cleopatra/styling/colorbar.py
def __init__(
    self,
    *,
    location: Literal["left", "right", "top", "bottom"] | None = None,
    orientation: Literal["vertical", "horizontal"] | None = None,
    inside: bool = False,
    box: bool | str | dict | None = None,
    label_color: str | None = None,
    tick_color: str | None = None,
    label: str | None = None,
    length: float | None = None,
    label_size: float | None = None,
    label_rotation: float | None = None,
    label_location: str | None = None,
    ticks_spacing: float | None = None,
) -> None:
    """Initialise a `ColorBar`.

    Args:
        location: Edge to sit on (`"left"`/`"right"`/`"top"`/`"bottom"`),
            or `None` for matplotlib's default placement.
        orientation: Bar orientation (`"vertical"`/`"horizontal"`), or
            `None` to let `location` decide (a vertical bar when neither is
            set). Ignored (with a `UserWarning`) when it disagrees with the
            orientation `location` implies.
        inside: Inset the colorbar inside the frame, by default `False`.
        box: Backing panel for an inside colorbar (`True` / colour / dict),
            or `None` to default it on when `inside` is set.
        label_color: Colour of the scale title / colorbar label (and the
            swatch title for a `style` preset); `None` keeps the default.
        tick_color: Colour of the colorbar's tick numbers; `None` keeps
            matplotlib's default.
        label: Caption text (scale title); `None` keeps the default.
        length: Bar length as a fraction of the axis; `None` keeps the
            default.
        label_size: Caption font size; `None` keeps the default.
        label_rotation: Caption rotation in degrees; `None` leaves
            matplotlib's own label orientation.
        label_location: Caption placement along the bar (distinct from
            `location`, the bar's edge); valid values depend on orientation
            (vertical: top/center/bottom, horizontal: left/center/right);
            `None` keeps the default.
        ticks_spacing: Spacing between the colorbar's ticks; `None` keeps
            the default.
    """
    _validate_orientation(orientation)
    _warn_orientation_conflict(location, orientation)
    _validate_label_location(location, orientation, label_location)
    self.location = location
    self.orientation = orientation
    self.inside = inside
    self.box = True if (inside and box is None) else box
    self.label_color = label_color
    self.tick_color = tick_color
    self.label = label
    self.length = length
    self.label_size = label_size
    self.label_rotation = label_rotation
    self.label_location = label_location
    self.ticks_spacing = ticks_spacing

reset_options() classmethod #

default_options updates for a default, sticky-clearing colorbar.

The dict colorbar=True applies: it draws a default bar and resets the resettable cbar_* family to STYLE_DEFAULTS, so a reused glyph does not inherit a prior sticky spec's placement or caption. Distinct from to_options, which maps a specific spec's fields and omits unset ones; this resets the whole cbar_* family to the defaults. ticks_spacing is deliberately excluded: it is glyph-specific (KDEGlyph, for one, auto-derives it from the data range when unset), so a single shared reset value could not restore each glyph's own default -- it is therefore left untouched by colorbar=True.

Returns:

Name Type Description
dict dict

default_options updates for a default colorbar.

Examples:

  • The reset always enables the bar and clears the placement:
    >>> from cleopatra.styling.colorbar import ColorBar
    >>> opts = ColorBar.reset_options()
    >>> opts["add_colorbar"]
    True
    >>> opts["cbar_location"] is None
    True
    
Source code in src/cleopatra/styling/colorbar.py
@classmethod
def reset_options(cls) -> dict:
    """`default_options` updates for a default, sticky-clearing colorbar.

    The dict `colorbar=True` applies: it draws a default bar and resets the
    resettable `cbar_*` family to `STYLE_DEFAULTS`, so a reused glyph does
    not inherit a prior sticky spec's placement or caption. Distinct from
    `to_options`, which maps a *specific* spec's fields and omits unset
    ones; this resets the whole `cbar_*` family to the defaults.
    `ticks_spacing` is deliberately excluded: it is glyph-specific
    (`KDEGlyph`, for one, auto-derives it from the data range when unset),
    so a single shared reset value could not restore each glyph's own
    default -- it is therefore left untouched by `colorbar=True`.

    Returns:
        dict: `default_options` updates for a default colorbar.

    Examples:
        - The reset always enables the bar and clears the placement:
            ```python
            >>> from cleopatra.styling.colorbar import ColorBar
            >>> opts = ColorBar.reset_options()
            >>> opts["add_colorbar"]
            True
            >>> opts["cbar_location"] is None
            True

            ```
    """
    return {
        "add_colorbar": True,
        "cbar_location": None,
        "cbar_inside": False,
        "cbar_box": None,
        "cbar_label_color": None,
        "cbar_tick_color": None,
        "cbar_orientation": STYLE_DEFAULTS["cbar_orientation"],
        "cbar_label": STYLE_DEFAULTS["cbar_label"],
        "cbar_length": STYLE_DEFAULTS["cbar_length"],
        "cbar_label_size": STYLE_DEFAULTS["cbar_label_size"],
        "cbar_label_rotation": STYLE_DEFAULTS["cbar_label_rotation"],
        "cbar_label_location": STYLE_DEFAULTS["cbar_label_location"],
    }

resolve(colorbar) classmethod #

Translate a colorbar= argument into default_options updates.

Owns the full None / False / True / ColorBar dispatch: None leaves the colorbar options untouched; False suppresses the bar; True resets to a default bar via reset_options; a ColorBar instance maps its fields via to_options.

Parameters:

Name Type Description Default
colorbar bool | ColorBar | None

None, False, True, or a ColorBar instance.

required

Returns:

Name Type Description
dict dict

Updates to merge into default_options (empty for None).

Raises:

Type Description
TypeError

If colorbar is not a bool, ColorBar, or None.

Examples:

>>> from cleopatra.styling.colorbar import ColorBar
>>> ColorBar.resolve(False)
{'add_colorbar': False}
>>> ColorBar.resolve(ColorBar(location="left", inside=True))["cbar_location"]
'left'
>>> "cbar_label" in ColorBar.resolve(ColorBar(location="right"))
False
Source code in src/cleopatra/styling/colorbar.py
@classmethod
def resolve(cls, colorbar: "bool | ColorBar | None") -> dict:
    """Translate a `colorbar=` argument into `default_options` updates.

    Owns the full `None` / `False` / `True` / `ColorBar` dispatch: `None`
    leaves the colorbar options untouched; `False` suppresses the bar;
    `True` resets to a default bar via `reset_options`; a `ColorBar`
    instance maps its fields via `to_options`.

    Args:
        colorbar: `None`, `False`, `True`, or a `ColorBar` instance.

    Returns:
        dict: Updates to merge into `default_options` (empty for `None`).

    Raises:
        TypeError: If `colorbar` is not a bool, `ColorBar`, or `None`.

    Examples:
        ```python
        >>> from cleopatra.styling.colorbar import ColorBar
        >>> ColorBar.resolve(False)
        {'add_colorbar': False}
        >>> ColorBar.resolve(ColorBar(location="left", inside=True))["cbar_location"]
        'left'
        >>> "cbar_label" in ColorBar.resolve(ColorBar(location="right"))
        False

        ```
    """
    if colorbar is None:
        return {}
    if colorbar is False:
        return {"add_colorbar": False}
    if colorbar is True:
        return cls.reset_options()
    if isinstance(colorbar, cls):
        return colorbar.to_options()
    raise TypeError(
        f"colorbar must be a bool, ColorBar, or None, got {type(colorbar).__name__}."
    )

specifies_placement() #

Whether this spec explicitly requests a placement or orientation.

True when any of location, inside, or orientation is set -- the spec asks for a specific colorbar rather than leaving the default. Used to decide whether a styled (preset) render should still draw a colorbar.

Returns:

Name Type Description
bool bool

True if location, inside, or orientation is set.

Examples:

  • A placement edge counts as specified; a bare spec does not:
    >>> from cleopatra.styling.colorbar import ColorBar
    >>> ColorBar(location="bottom").specifies_placement()
    True
    >>> ColorBar().specifies_placement()
    False
    
Source code in src/cleopatra/styling/colorbar.py
def specifies_placement(self) -> bool:
    """Whether this spec explicitly requests a placement or orientation.

    `True` when any of `location`, `inside`, or `orientation` is set -- the
    spec asks for a specific colorbar rather than leaving the default. Used
    to decide whether a styled (preset) render should still draw a colorbar.

    Returns:
        bool: `True` if `location`, `inside`, or `orientation` is set.

    Examples:
        - A placement edge counts as specified; a bare spec does not:
            ```python
            >>> from cleopatra.styling.colorbar import ColorBar
            >>> ColorBar(location="bottom").specifies_placement()
            True
            >>> ColorBar().specifies_placement()
            False

            ```
    """
    return (
        self.location is not None
        or self.inside
        or self.orientation is not None
    )

to_options() #

Map this spec's fields onto the cbar_* default_options keys.

Mirrors the other grouped styling objects' to_options: the object owns the translation from its own fields to the flat render options create_color_bar reads. Placement fields are always emitted (so a reused glyph's prior placement is overwritten); the caption / sizing / orientation / tick-spacing fields are emitted only when set, leaving an unset field at the existing default.

Returns:

Name Type Description
dict dict

default_options updates for this spec, always including add_colorbar=True.

Examples:

  • Placement maps onto cbar_*; unset caption fields are omitted:
    >>> from cleopatra.styling.colorbar import ColorBar
    >>> ColorBar(location="left", inside=True).to_options()["cbar_location"]
    'left'
    >>> "cbar_label" in ColorBar(location="right").to_options()
    False
    
Source code in src/cleopatra/styling/colorbar.py
def to_options(self) -> dict:
    """Map this spec's fields onto the `cbar_*` `default_options` keys.

    Mirrors the other grouped styling objects' `to_options`: the object
    owns the translation from its own fields to the flat render options
    `create_color_bar` reads. Placement fields are always emitted (so a
    reused glyph's prior placement is overwritten); the caption / sizing /
    orientation / tick-spacing fields are emitted only when set, leaving an
    unset field at the existing default.

    Returns:
        dict: `default_options` updates for this spec, always including
            `add_colorbar=True`.

    Examples:
        - Placement maps onto `cbar_*`; unset caption fields are omitted:
            ```python
            >>> from cleopatra.styling.colorbar import ColorBar
            >>> ColorBar(location="left", inside=True).to_options()["cbar_location"]
            'left'
            >>> "cbar_label" in ColorBar(location="right").to_options()
            False

            ```
    """
    updates = {
        "add_colorbar": True,
        "cbar_location": self.location,
        "cbar_inside": self.inside,
        "cbar_box": self.box,
        "cbar_label_color": self.label_color,
        "cbar_tick_color": self.tick_color,
    }
    optional = {
        "cbar_label": self.label,
        "cbar_length": self.length,
        "cbar_label_size": self.label_size,
        "cbar_label_rotation": self.label_rotation,
        "cbar_label_location": self.label_location,
        "cbar_orientation": self.orientation,
        "ticks_spacing": self.ticks_spacing,
    }
    updates.update({k: v for k, v in optional.items() if v is not None})
    return updates

Composing onto one axes (compose=)#

By default a glyph drawn onto an axes replaces whatever a glyph put there before it. That is deliberate: a second glyph bound to an existing axes would otherwise leave the first one's artists attached and driven by nothing, which is what once froze an animation at its first frame.

Pass compose=True to draw over what is already there instead — the classic scalar field with wind arrows on top:

fig, ax = plt.subplots()
temperature.plot(ax=ax, colorbar=ColorBar(label="500 hPa T [C]"))
VectorGlyph(xx, yy, u, v, ax=ax, thin=4).plot(kind="quiver", ax=ax, compose=True)

Three methods accept it: ArrayGlyph.plot, ArrayGlyph.animate and VectorGlyph.plot. Every other glyph replaces unconditionally, so passing compose= to one of them is a TypeError.

A composing render clears only the artists it put there itself, so the host's layers, colorbar, title, ticks and projection frame all survive. A glyph replotting onto its own axes still replaces its own artists either way, so nothing is orphaned.

The overlay also draws no colorbar of its own, since a second colorbar would take its space from the host axes and re-lay it out on every overlay. Pass add_colorbar=True (or a colorbar= spec) if the overlay should have one anyway.

Thinning a vector field (thin=)#

quiver and barbs draw one arrow per grid point, which on a real grid is both unreadable and slow — a 141x321 window is 45,261 arrows. thin=n draws every nth point along each axis:

VectorGlyph(xx, yy, u, v, thin=4).plot(kind="quiver")

thin is a construction-time option like density and scale, not a plot() argument.

It applies to quiver and barbs. streamplot seeds its own lines and has no per-point arrow to drop, so thin warns there — use density= instead.