Skip to content

API reference — Specs

behaviz.spec.plot_spec

PlotSpec dataclass

Master spec object. Compose sub-specs for full control, or rely on defaults and override only what you need.

Quick-start

spec = PlotSpec(title="My Plot", x=AxisSpec(label="Time", unit="s")) plot_line(ax, t, v, spec)

Source code in behaviz/spec/plot_spec.py
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
@dataclass
class PlotSpec:
    """
    Master spec object.  Compose sub-specs for full control, or rely on
    defaults and override only what you need.

    Quick-start
    -----------
    >>> spec = PlotSpec(title="My Plot", x=AxisSpec(label="Time", unit="s"))
    >>> plot_line(ax, t, v, spec)
    """

    # -- Identity
    title: str = ""
    title_fontsize: Optional[float] = None  # None → x.fontsize + 2
    text_color: Optional[str] = None  # labels, title, tick labels; None → backend default

    # -- Axes
    x: AxisSpec = field(default_factory=AxisSpec)
    y: AxisSpec = field(default_factory=AxisSpec)

    # -- Figure (only used when the spec creates its own figure)
    figure: FigureSpec = field(default_factory=FigureSpec)

    # -- Legend
    show_legend: bool = False
    legend_pos: LegendPosition = LegendPosition.BEST
    legend_fontsize: Optional[float] = None  # None → backend default

    # -- Annotations
    annotations: list[dict] = field(default_factory=list)
    # each dict: {"x": val, "y": val, "text": str, "kwargs": {...}}

    # -- Post-processing hook
    # Signature: hook(ax: Axes, spec: PlotSpec) -> None
    post_hook: Optional[Callable] = None

    @classmethod
    def preset(
        cls,
        name: Literal[
            "paper",
            "poster",
            "notebook",
            "dark",
            "presentation",
            "presentation_dark",
            "print",
            "custom",
        ],
        style_dict: dict | None = None,
    ) -> "PlotSpec":
        """
        Return a PlotSpec tuned for a specific output target.

        paper    → small figure, thin lines, no grid
        poster   → large figure, thick lines, big fonts
        notebook → medium figure, grid on, slightly transparent markers
        dark     → dark background, bright colours
        paper             → small figure, thin lines, no grid
        poster            → large figure, thick lines, big fonts
        notebook          → medium figure, grid on, slightly transparent markers
        dark              → dark background, bright colours
        presentation      → large 12x12in slide figure, thick lines, 24pt fonts
        presentation_dark → presentation on a #1E1E1E dark background, muted colors
        print             → 8x8cm Word/print figure, thin lines, 14pt fonts
        """
        if name in RC_PRESETS:
            return cls._from_rcparams(RC_PRESETS[name])

        if name == "paper":
            return cls(
                figure=FigureSpec(figsize=(5 * CM, 5 * CM), dpi=300, style="seaborn-v0_8-paper"),
                x=AxisSpec(grid=False),
                y=AxisSpec(grid=False),
            )
        elif name == "poster":
            return cls(
                figure=FigureSpec(figsize=(15 * CM, 15 * CM), dpi=300, style="seaborn-v0_8-talk"),
            )
        elif name == "notebook":
            return cls(
                figure=FigureSpec(figsize=(8, 5), dpi=100, style="seaborn-v0_8-whitegrid"),
                x=AxisSpec(grid=True, grid_minor=True),
                y=AxisSpec(grid=True, grid_minor=True),
            )
        elif name == "dark":
            return cls(
                figure=FigureSpec(figsize=(8, 5), dpi=120, style="dark_background"),
            )
        elif name == "custom":
            assert isinstance(style_dict, dict), (
                f"Custom style requires a custom style dictionary to be provided, got {style_dict}"
            )
            return cls(
                figure=FigureSpec(
                    figsize=style_dict.get("figure.figsize", (12, 12)),
                    dpi=style_dict.get("figure.dpi", 300),
                    style=style_dict,
                )
            )
        else:
            raise ValueError(
                f"Unknown preset '{name}'. Choose: paper | poster | notebook | dark. Or provide a custom style dictionary"
            )

    @classmethod
    def _from_rcparams(cls, rc: dict) -> "PlotSpec":
        """Build a PlotSpec from a raw matplotlib rcParams dict.

        The full dict is still stored on ``FigureSpec.style`` (applied via
        ``plt.style.use`` on matplotlib/seaborn), but the visual properties are
        also mirrored onto first-class spec fields so the **bokeh** backend —
        which has no rcParams — renders the same preset. Grid is left off,
        matching these styles (they define grid appearance but never set
        ``axes.grid``).
        """
        labelsize = rc.get("axes.labelsize", 12)
        spines = [
            side
            for side, key in (
                ("bottom", "axes.spines.bottom"),
                ("left", "axes.spines.left"),
                ("top", "axes.spines.top"),
                ("right", "axes.spines.right"),
            )
            if rc.get(key, True)
        ]
        axis = lambda p: AxisSpec(  # noqa: E731  (p = "x"/"y" tick prefix)
            fontsize=labelsize,
            spines=list(spines),
            grid=False,
            spine_width=rc.get("axes.linewidth", 2),
            spine_color=rc.get("axes.edgecolor"),
            tick_color=rc.get(f"{p}tick.color"),
            tick_length=rc.get(f"{p}tick.major.size"),
            tick_width=rc.get(f"{p}tick.major.width"),
            grid_color=rc.get("grid.color", "#c1c1c1"),
            grid_alpha=rc.get("grid.alpha", 0.5),
            grid_width=rc.get("grid.linewidth", 0.8),
            grid_style=rc.get("grid.linestyle", "-"),
        )
        return cls(
            text_color=rc.get("text.color") or rc.get("axes.labelcolor"),
            figure=FigureSpec(
                figsize=rc.get("figure.figsize", (12, 12)),
                dpi=rc.get("figure.dpi", 300),
                style=rc,
                face_color=rc.get("figure.facecolor"),
                axes_color=rc.get("axes.facecolor"),
            ),
            x=axis("x"),
            y=axis("y"),
        )

    @classmethod
    def from_labels(cls, xlabel: str, ylabel: str, xunit: str = "", yunit: str = "", **kwargs) -> "PlotSpec":
        """Shortest path when you only care about axis labels."""
        return cls(
            x=AxisSpec(label=xlabel, unit=xunit),
            y=AxisSpec(label=ylabel, unit=yunit),
            **kwargs,
        )

    # ==============================
    # Mutation-free override helpers
    # ==============================
    def with_title(self, title: str) -> "PlotSpec":
        return replace(self, title=title)

    def with_xlabel(self, label: str) -> "PlotSpec":
        return replace(self, x=replace(self.x, label=label))

    def with_ylabel(self, label: str) -> "PlotSpec":
        return replace(self, y=replace(self.y, label=label))

    def with_xlim(self, lo, hi) -> "PlotSpec":
        return replace(self, x=replace(self.x, lim=(lo, hi)))

    def with_ylim(self, lo, hi) -> "PlotSpec":
        return replace(self, y=replace(self.y, lim=(lo, hi)))

    def with_xticks(self, ticks: list, tick_fmt: str = None) -> "PlotSpec":
        new = replace(self, x=replace(self.x, ticks=ticks))
        new = replace(new, x=replace(new.x, tick_fmt=tick_fmt))
        return new

    def with_yticks(self, ticks: list, tick_fmt: str = None) -> "PlotSpec":
        new = replace(self, y=replace(self.y, ticks=ticks))
        new = replace(new, y=replace(new.y, tick_fmt=tick_fmt))
        return new

    def with_fontsize(self, fontsize: float, axis: Literal["x", "y", "both"] = "both") -> "PlotSpec":
        new = self
        if axis in ("x", "both"):
            new = replace(new, x=replace(new.x, fontsize=fontsize))

        if axis in ("y", "both"):
            new = replace(new, y=replace(new.y, fontsize=fontsize))
        return new

    def with_scale(
        self, axis: Literal["x", "y", "both"], scale_type: Literal["linear", "log", "logit", "symlog"]
    ) -> "PlotSpec":
        new = self
        if axis in ("x", "both"):
            new = replace(new, x=replace(new.x, scale=ScaleType(scale_type)))
        if axis in ("y", "both"):
            new = replace(new, y=replace(new.y, scale=ScaleType(scale_type)))
        return new

    def with_size(self, figsize: tuple[int, int]) -> "PlotSpec":
        return replace(self, figure=replace(self.figure, figsize=figsize))

    def with_annotation(self, x, y, text: str, **kwargs) -> "PlotSpec":
        new_annotations = self.annotations + [{"x": x, "y": y, "text": text, "kwargs": kwargs}]
        return replace(self, annotations=new_annotations)

    def with_hook(self, fn: Callable) -> "PlotSpec":
        return replace(self, post_hook=fn)

preset classmethod

preset(name, style_dict=None)

Return a PlotSpec tuned for a specific output target.

paper → small figure, thin lines, no grid poster → large figure, thick lines, big fonts notebook → medium figure, grid on, slightly transparent markers dark → dark background, bright colours paper → small figure, thin lines, no grid poster → large figure, thick lines, big fonts notebook → medium figure, grid on, slightly transparent markers dark → dark background, bright colours presentation → large 12x12in slide figure, thick lines, 24pt fonts presentation_dark → presentation on a #1E1E1E dark background, muted colors print → 8x8cm Word/print figure, thin lines, 14pt fonts

Source code in behaviz/spec/plot_spec.py
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
@classmethod
def preset(
    cls,
    name: Literal[
        "paper",
        "poster",
        "notebook",
        "dark",
        "presentation",
        "presentation_dark",
        "print",
        "custom",
    ],
    style_dict: dict | None = None,
) -> "PlotSpec":
    """
    Return a PlotSpec tuned for a specific output target.

    paper    → small figure, thin lines, no grid
    poster   → large figure, thick lines, big fonts
    notebook → medium figure, grid on, slightly transparent markers
    dark     → dark background, bright colours
    paper             → small figure, thin lines, no grid
    poster            → large figure, thick lines, big fonts
    notebook          → medium figure, grid on, slightly transparent markers
    dark              → dark background, bright colours
    presentation      → large 12x12in slide figure, thick lines, 24pt fonts
    presentation_dark → presentation on a #1E1E1E dark background, muted colors
    print             → 8x8cm Word/print figure, thin lines, 14pt fonts
    """
    if name in RC_PRESETS:
        return cls._from_rcparams(RC_PRESETS[name])

    if name == "paper":
        return cls(
            figure=FigureSpec(figsize=(5 * CM, 5 * CM), dpi=300, style="seaborn-v0_8-paper"),
            x=AxisSpec(grid=False),
            y=AxisSpec(grid=False),
        )
    elif name == "poster":
        return cls(
            figure=FigureSpec(figsize=(15 * CM, 15 * CM), dpi=300, style="seaborn-v0_8-talk"),
        )
    elif name == "notebook":
        return cls(
            figure=FigureSpec(figsize=(8, 5), dpi=100, style="seaborn-v0_8-whitegrid"),
            x=AxisSpec(grid=True, grid_minor=True),
            y=AxisSpec(grid=True, grid_minor=True),
        )
    elif name == "dark":
        return cls(
            figure=FigureSpec(figsize=(8, 5), dpi=120, style="dark_background"),
        )
    elif name == "custom":
        assert isinstance(style_dict, dict), (
            f"Custom style requires a custom style dictionary to be provided, got {style_dict}"
        )
        return cls(
            figure=FigureSpec(
                figsize=style_dict.get("figure.figsize", (12, 12)),
                dpi=style_dict.get("figure.dpi", 300),
                style=style_dict,
            )
        )
    else:
        raise ValueError(
            f"Unknown preset '{name}'. Choose: paper | poster | notebook | dark. Or provide a custom style dictionary"
        )

from_labels classmethod

from_labels(xlabel, ylabel, xunit='', yunit='', **kwargs)

Shortest path when you only care about axis labels.

Source code in behaviz/spec/plot_spec.py
167
168
169
170
171
172
173
174
@classmethod
def from_labels(cls, xlabel: str, ylabel: str, xunit: str = "", yunit: str = "", **kwargs) -> "PlotSpec":
    """Shortest path when you only care about axis labels."""
    return cls(
        x=AxisSpec(label=xlabel, unit=xunit),
        y=AxisSpec(label=ylabel, unit=yunit),
        **kwargs,
    )

behaviz.spec.axis_spec

AxisSpec dataclass

Everything that describes a single axis.

Source code in behaviz/spec/axis_spec.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@dataclass
class AxisSpec:
    """Everything that describes a single axis."""

    label: str = ""
    unit: str = ""  # appended automatically: "Voltage (mV)"
    fontsize: float = 12
    scale: ScaleType = ScaleType.LINEAR
    lim: Optional[tuple] = None  # (min, max) or None → auto
    ticks: Optional[list] = None  # explicit tick positions
    tick_fmt: Optional[str] = None  # e.g. "%.2f", "{x:.1e}"
    invert: bool = False  # flip axis direction
    spines: list[Literal["bottom", "top", "left", "right"]] = field(
        default_factory=lambda: ["bottom", "top", "left", "right"]
    )
    spine_width: float = 2
    spine_color: Optional[str] = None  # None → backend default
    spine_offset: float = 0  # push spines outward from the axes (matplotlib only)
    spine_trim: bool = False  # clip spines to the data range (matplotlib only)
    tick_dir: Literal["out", "in", "inout"] = "out"
    tick_length: Optional[float] = None  # None → 3 × spine_width
    tick_width: Optional[float] = None  # None → spine_width
    tick_color: Optional[str] = None  # None → backend default
    tick_sides: Optional[list] = None  # which sides show tick marks; None → backend default
    grid: bool = True
    grid_minor: bool = False
    grid_alpha: float = 0.5
    grid_color: str = "#c1c1c1"
    grid_style: str = "-"  # major grid linestyle ("-", "--", ":", "-.")
    grid_width: float = 0.8  # major grid linewidth

    @property
    def full_label(self) -> str:
        """Return 'Label (unit)' or just 'Label' when no unit is set."""
        return f"{self.label} ({self.unit})" if self.unit else self.label

full_label property

full_label

Return 'Label (unit)' or just 'Label' when no unit is set.

behaviz.spec.figure_spec

FigureSpec dataclass

Figure-level properties.

Source code in behaviz/spec/figure_spec.py
15
16
17
18
19
20
21
22
23
24
25
@dataclass
class FigureSpec:
    """Figure-level properties."""

    figsize: tuple = (12, 8)
    dpi: int = 120
    tight: bool = True  # call tight_layout automatically
    style: str | dict = "default"  # any valid plt.style name or custom style dictionary
    face_color: Optional[str] = None  # figure background; None → backend default
    axes_color: Optional[str] = None  # axes/plot-area background; None → backend default
    font_family: Optional[str] = None  # font family for all text; None → backend default

behaviz.spec.colorbar_spec

ColorbarSpec dataclass

Styling for a colorbar attached to a colour-mapped plot (e.g. plot_image).

Pass it to a plot's colorbar= keyword, or use the shorthands handled by :meth:coerce: colorbar=True for a default bar, colorbar="label" for a labelled one.

Fields

label : str Text shown beside the bar. location : "right" | "left" | "top" | "bottom" Side of the axes to place the bar on (sets orientation automatically). ticks : list | None Explicit tick positions. None → automatic. tick_fmt : str | None printf-style tick format, e.g. "%.1f". fraction : float matplotlib sizing. The default (0.046) makes the bar match the axes height — the usual "magic number" — so users don't have to. pad : float | None Gap between axes and bar (axes-fraction). None picks a location-aware default that clears the axis labels (small on the right, larger on the bottom/left where tick labels live). fontsize : float Label and tick-label size.

Source code in behaviz/spec/colorbar_spec.py
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@dataclass
class ColorbarSpec:
    """Styling for a colorbar attached to a colour-mapped plot (e.g. ``plot_image``).

    Pass it to a plot's ``colorbar=`` keyword, or use the shorthands handled by
    :meth:`coerce`: ``colorbar=True`` for a default bar, ``colorbar="label"`` for
    a labelled one.

    Fields
    ------
    label : str
        Text shown beside the bar.
    location : "right" | "left" | "top" | "bottom"
        Side of the axes to place the bar on (sets orientation automatically).
    ticks : list | None
        Explicit tick positions. ``None`` → automatic.
    tick_fmt : str | None
        printf-style tick format, e.g. ``"%.1f"``.
    fraction : float
        matplotlib sizing. The default (0.046) makes the bar match the axes
        height — the usual "magic number" — so users don't have to.
    pad : float | None
        Gap between axes and bar (axes-fraction). ``None`` picks a location-aware
        default that clears the axis labels (small on the right, larger on the
        bottom/left where tick labels live).
    fontsize : float
        Label and tick-label size.
    """

    label: str = ""
    location: Literal["right", "left", "top", "bottom"] = "right"
    ticks: Optional[list] = None
    tick_fmt: Optional[str] = None
    fraction: float = 0.046
    pad: Optional[float] = None
    fontsize: float = 12

    def resolved_pad(self) -> float:
        """The pad to use — explicit if set, else a location-aware default."""
        if self.pad is not None:
            return self.pad
        return _AUTO_PAD.get(self.location, 0.05)

    @classmethod
    def coerce(cls, value) -> "ColorbarSpec":
        """Normalise a colorbar argument (True/str/spec) to a spec."""
        if isinstance(value, cls):
            return value
        if isinstance(value, str):
            return cls(label=value)
        return cls()

resolved_pad

resolved_pad()

The pad to use — explicit if set, else a location-aware default.

Source code in behaviz/spec/colorbar_spec.py
45
46
47
48
49
def resolved_pad(self) -> float:
    """The pad to use — explicit if set, else a location-aware default."""
    if self.pad is not None:
        return self.pad
    return _AUTO_PAD.get(self.location, 0.05)

coerce classmethod

coerce(value)

Normalise a colorbar argument (True/str/spec) to a spec.

Source code in behaviz/spec/colorbar_spec.py
51
52
53
54
55
56
57
58
@classmethod
def coerce(cls, value) -> "ColorbarSpec":
    """Normalise a colorbar argument (True/str/spec) to a spec."""
    if isinstance(value, cls):
        return value
    if isinstance(value, str):
        return cls(label=value)
    return cls()