Skip to content

API reference — Manipulations

behaviz.manipulations.jitter

BeeswarmJitter

Bases: _JitterStrategy

Dot-swarm layout (replaces the free make_dot_swarm function).

Source code in behaviz/manipulations/jitter.py
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
class BeeswarmJitter(_JitterStrategy):
    """Dot-swarm layout (replaces the free make_dot_swarm function)."""

    def apply(
        self,
        x,
        y,
        rng,
        *,
        axis="x",
        center: float = 0.0,
        bin_width: float = 50.0,
        width: float = 0.5,
        side="center",
        **_,
    ):
        if axis == "x":
            x = x + self._beeswarm_coords(y, center=center, bin_width=bin_width, width=width, side=side)
        elif axis == "y":
            y = y + self._beeswarm_coords(x, center=center, bin_width=bin_width, width=width, side=side)
        else:
            raise ValueError(f"Unknown coordinate axis {axis}")
        return x, y  # y is unchanged; x is the dispersed axis

    @staticmethod
    def _beeswarm_coords(
        data: np.ndarray,
        center: float,
        bin_width: float,
        width: float,
        side: Literal["left", "right", "center"] = "center",
    ) -> np.ndarray:
        """Pure function — direct replacement for make_dot_swarm in rainplot.py."""
        if len(data) <= 1:
            return np.array([center])
        bin_edges = np.arange(np.nanmin(data), np.nanmax(data) + bin_width, bin_width)
        counts, bin_edges = np.histogram(data, bins=bin_edges)
        max_count = np.nanmax(counts) // 2
        dx = width / max_count if max_count else 0
        idx_in_bin = []
        for k, (ymin, ymax) in enumerate(zip(bin_edges[:-1], bin_edges[1:])):
            mask = (data >= ymin) & (data <= ymax if k == len(bin_edges) - 2 else data < ymax)
            idx_in_bin.append(np.nonzero(mask)[0])
        x_coords = np.zeros(len(data))
        for i in idx_in_bin:
            if len(i) > 1:
                n, j = len(i), len(i) % 2
                left_half = i[: n // 2][::-1]
                right_half = i[n // 2 + j :]
                for rank, (ll, rr) in enumerate(zip(left_half, right_half)):
                    offset = (rank + (j == 0) * 0.5) * dx
                    x_coords[ll] = -offset if side != "right" else offset
                    x_coords[rr] = offset if side != "left" else -offset
        return x_coords + center

behaviz.manipulations.smoother

behaviz.manipulations.normaliser

behaviz.manipulations.binner

behaviz.manipulations.dodger

Dodging: arrange side-by-side categories that share an x position.

A deterministic positioning transform (no RNG/state) used to place grouped bars or error bars relative to each other instead of on top of one another. Like the other manipulations it is a small strategy family — but with its own contract (n_levels → placement rather than (x, y) → (x, y)), so it is not wired into VisualManipulator; the grouping engine selects a strategy by name.

Strategies

centered side-by-side: tile n equal slots centered on each x. stacked each level sits on the cumulative height of the levels below it.

DodgePlacement dataclass

How one level should be drawn. None fields leave the default in place.

Source code in behaviz/manipulations/dodger.py
41
42
43
44
45
46
47
@dataclass(frozen=True)
class DodgePlacement:
    """How one level should be drawn. ``None`` fields leave the default in place."""

    x: np.ndarray
    width: float | None = None
    bottom: np.ndarray | None = None

CenteredDodge

Bases: _DodgeStrategy

Side-by-side bars/markers: equal slots tiled and centered on each x.

Source code in behaviz/manipulations/dodger.py
76
77
78
79
80
81
class CenteredDodge(_DodgeStrategy):
    """Side-by-side bars/markers: equal slots tiled and centered on each x."""

    def place(self, level, n_levels, x, y, *, total_width, state):
        offsets, width = dodge_offsets(n_levels, total_width)
        return DodgePlacement(x=np.asarray(x, dtype=float) + offsets[level], width=width)

StackedDodge

Bases: _DodgeStrategy

Stacked bars: levels share x, each sitting on the cumulative height below.

Source code in behaviz/manipulations/dodger.py
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
class StackedDodge(_DodgeStrategy):
    """Stacked bars: levels share x, each sitting on the cumulative height below."""

    needs_bottom = True

    def place(
        self,
        level,
        n_levels,
        x,
        y,
        *,
        total_width,
        state,
    ):
        x = np.asarray(x, dtype=float)
        y = np.asarray(y, dtype=float)
        running: dict = state.setdefault("running", {})
        keys = [round(float(xi), 9) for xi in x]  # tolerate float x positions
        bottom = np.array([running.get(k, 0.0) for k in keys], dtype=float)
        for k, yi in zip(keys, y):
            running[k] = running.get(k, 0.0) + float(yi)
        return DodgePlacement(x=x, width=total_width, bottom=bottom)

dodge_offsets

dodge_offsets(n_levels, total_width=0.8)

Tile n_levels slots, centered on each x position.

Returns (offsets, width) — the x offset per level (symmetric about 0) and the per-level width (total_width / n_levels).

dodge_offsets(1) ([0.0], 0.8) dodge_offsets(2, total_width=0.8) ([-0.2, 0.2], 0.4)

Source code in behaviz/manipulations/dodger.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
def dodge_offsets(n_levels: int, total_width: float = 0.8) -> tuple[list[float], float]:
    """Tile ``n_levels`` slots, centered on each x position.

    Returns ``(offsets, width)`` — the x offset per level (symmetric about 0)
    and the per-level width (``total_width / n_levels``).

    >>> dodge_offsets(1)
    ([0.0], 0.8)
    >>> dodge_offsets(2, total_width=0.8)
    ([-0.2, 0.2], 0.4)
    """
    if n_levels < 1:
        raise ValueError(f"n_levels must be >= 1, got {n_levels}.")
    width = total_width / n_levels
    offsets = [(i - (n_levels - 1) / 2) * width for i in range(n_levels)]
    return offsets, width

get_dodge

get_dodge(name)

Look up a dodge strategy by name.

Source code in behaviz/manipulations/dodger.py
121
122
123
124
125
126
def get_dodge(name: str) -> _DodgeStrategy:
    """Look up a dodge strategy by name."""
    try:
        return _DODGE_STRATEGIES[name]
    except KeyError:
        raise ValueError(f"Unknown dodge {name!r}. Choose: {', '.join(sorted(_DODGE_STRATEGIES))}.") from None