# PairPlot API


`PairPlot` shows every pair of numeric columns as a scatter plot, with a histogram of each column on the diagonal. Select rows in one cell and they light up in all the others.


```
import numpy as np
from wigglystuff import PairPlot

X = np.random.default_rng(0).normal(size=(1_000_000, 8))
labels = np.random.default_rng(1).integers(0, 5, size=len(X))

plot = PairPlot(X, color=labels)
plot
```


DataFrames work too. `color` can then be a column name, and the other numeric columns become the dimensions (pick them with `columns=`):


```
PairPlot(df, color="species")
PairPlot(df, columns=["bill_length", "bill_depth", "body_mass"], color="species")
```


## Selecting rows



- **Drag** inside any scatter cell to draw a **box**, or switch to **Lasso** in the toolbar (or pass `mode="lasso"`) for a freehand outline.

- **Drag a selection** to move it. Every cell and histogram updates while you drag; Python receives the new `selection` when you let go.

- Selected rows keep their category color; everything else turns gray. The diagonal shows the selection stacked on top of the full histogram.

- **Click** outside the selection, double-click, or press **Clear** to clear it.


The selection is synced as a small description in data coordinates, not as a list of row indices, so it stays cheap at a million rows:


```
{"type": "box", "x_index": 0, "y_index": 1,
 "x_min": -1.2, "x_max": 0.8, "y_min": -0.5, "y_max": 2.1}
{"type": "lasso", "x_index": 0, "y_index": 1, "vertices": [[x, y], ...]}
```


Python evaluates it against the full data, so these are exact:


```
plot.selected_count    # number of selected rows
plot.get_mask()        # boolean NumPy mask
plot.get_indices()     # integer row indices
plot.selected_data     # selected rows, same type as the input
```


## Keep, Exclude, Reset


Like the parallel coordinates widget, a selection can become a filter:



- **Keep** drops every row outside the selection.

- **Exclude** drops every row inside it.

- **Reset** brings every row back.


After a filter the axes and histograms animate to the rows that remain, which works as a zoom. Filters stack and are synced as the `filters` list, which Python replays to get exact results. The same actions exist in Python:


```
plot.keep()               # or plot.exclude()
plot.get_active_mask()    # rows left after the filters
plot.active_data          # those rows, same type as the input
plot.reset()
```


New selections only count rows that are still in the plot.


## How it scales


The numbers go to the browser once, as binary float32, and stay in a Web Worker. WebGL draws a sample of at most `max_marks` points spread across the cells (with 8 dimensions, about 36,000 per cell), while counts, histograms and selections use every row. At a million rows a selection takes a few tens of milliseconds, so a box keeps up while you drag it. Point size and opacity adapt to how crowded a cell is unless you set `point_size`.


Non-finite values are skipped in the axes, histograms and selections. Up to eight categories get distinct colors by default; pass `color_map` to choose your own.


 Bases: `AnyWidget`


Interactive scatterplot matrix with linked box and lasso selection.


`PairPlot` sends numeric data to the browser as binary float32 data and keeps the full matrix in a Web Worker. The visible plot is bounded by a global mark budget, while a selection is evaluated against every row.



The selection is synced as a compact data-coordinate description (box corners or lasso vertices). Use :meth:`get_mask`, :meth:`get_indices`, or :attr:`selected_data` to obtain exact Python-side results without syncing a potentially huge index list.


Like HiPlot, a selection can be turned into a filter: **Keep** drops every row outside it and **Exclude** drops every row inside it, after which the axes rescale to the rows that remain. Filters stack, are synced as the replayable `filters` list, and **Reset** removes them all. The same actions are available from Python as :meth:`keep`, :meth:`exclude`, and :meth:`reset`; :attr:`active_data` holds the rows that survive the filters.

 Source code in `wigglystuff/pair_plot.py`

```
def __init__(
    self,
    data: Any,
    *,
    columns: Sequence[str] | None = None,
    color: Any = None,
    color_map: Mapping[Any, str] | None = None,
    layout: str = "full",
    mode: str = "box",
    cell_size: int | None = None,
    max_marks: int = 2_000_000,
    point_size: float | None = None,
    show_filters: bool = True,
    show_mode_toggle: bool = True,
    show_layout_toggle: bool = True,
    **kwargs: Any,
) -> None:
    matrix, names, color_values, source_kind = _normalize_input(
        data, columns=columns, color=color
    )
    codes, category_labels, palette = _encode_colors(
        color_values, color_map, matrix.shape[0]
    )

    self._source_data = data
    self._source_kind = source_kind
    self._matrix = matrix
    self._color_codes = codes

    super().__init__(
        columns=names,
        categories=category_labels,
        palette=palette,
        layout=layout,
        mode=mode,
        cell_size=cell_size,
        max_marks=max_marks,
        point_size=point_size,
        show_filters=show_filters,
        show_mode_toggle=show_mode_toggle,
        show_layout_toggle=show_layout_toggle,
        **kwargs,
    )
    self.on_msg(self._handle_frontend_message)
```


## active_data `property`


```
active_data
```


Return the rows left after Keep/Exclude filters, like the input type.


## selected_data `property`


```
selected_data
```


Return selected rows in the same broad container type as the input.


## shape `property`


```
shape: tuple[int, int]
```


The numeric matrix shape as `(rows, dimensions)`.


## clear_selection


```
clear_selection() -> None
```


Clear the selection and restore the unfiltered color display.

 Source code in `wigglystuff/pair_plot.py`

```
def clear_selection(self) -> None:
    """Clear the selection and restore the unfiltered color display."""
    self.selection = {}
    self.selected_count = 0
```


## exclude


```
exclude() -> None
```


Drop the rows inside the current selection (a no-op without one).

 Source code in `wigglystuff/pair_plot.py`

```
def exclude(self) -> None:
    """Drop the rows inside the current selection (a no-op without one)."""
    self._add_filter("exclude")
```


## get_active_mask


```
get_active_mask()
```


Return a boolean NumPy mask of the rows left after all filters.

 Source code in `wigglystuff/pair_plot.py`

```
def get_active_mask(self):
    """Return a boolean NumPy mask of the rows left after all filters."""
    import numpy as np

    active = np.ones(self._matrix.shape[0], dtype=bool)
    for step in self.filters:
        inside = self._selection_mask(step.get("selection", {}))
        if step.get("action") == "keep":
            active &= inside
        elif step.get("action") == "exclude":
            active &= ~inside
    return active
```


## get_indices


```
get_indices()
```


Return exact integer row indices inside the current selection.

 Source code in `wigglystuff/pair_plot.py`

```
def get_indices(self):
    """Return exact integer row indices inside the current selection."""
    import numpy as np

    return np.flatnonzero(self.get_mask())
```


## get_mask


```
get_mask()
```


Return an exact boolean NumPy mask for the current selection.


Only rows that survive the Keep/Exclude filters can be selected.

 Source code in `wigglystuff/pair_plot.py`

```
def get_mask(self):
    """Return an exact boolean NumPy mask for the current selection.

    Only rows that survive the Keep/Exclude filters can be selected.
    """
    mask = self._selection_mask(self.selection)
    if self.filters and mask.any():
        mask &= self.get_active_mask()
    return mask
```


## keep


```
keep() -> None
```


Keep only the rows inside the current selection (a no-op without one).

 Source code in `wigglystuff/pair_plot.py`

```
def keep(self) -> None:
    """Keep only the rows inside the current selection (a no-op without one)."""
    self._add_filter("keep")
```


## reset


```
reset() -> None
```


Remove every Keep/Exclude filter and clear the selection.

 Source code in `wigglystuff/pair_plot.py`

```
def reset(self) -> None:
    """Remove every Keep/Exclude filter and clear the selection."""
    self.filters = []
    self.clear_selection()
```


## set_data


```
set_data(data: Any, *, columns: Sequence[str] | None = None, color: Any = None, color_map: Mapping[Any, str] | None = None) -> None
```


Replace the displayed dataset and clear the current selection.

 Source code in `wigglystuff/pair_plot.py`

```
def set_data(
    self,
    data: Any,
    *,
    columns: Sequence[str] | None = None,
    color: Any = None,
    color_map: Mapping[Any, str] | None = None,
) -> None:
    """Replace the displayed dataset and clear the current selection."""
    matrix, names, color_values, source_kind = _normalize_input(
        data, columns=columns, color=color
    )
    codes, category_labels, palette = _encode_colors(
        color_values, color_map, matrix.shape[0]
    )

    self._source_data = data
    self._source_kind = source_kind
    self._matrix = matrix
    self._color_codes = codes
    self.columns = names
    self.categories = category_labels
    self.palette = palette
    self.filters = []
    self.clear_selection()
    self.status = "loading"
    self.error = ""
    # Each view answers a data_version bump with a "ready" message, which
    # makes Python send the new buffers.
    self.data_version += 1
```


## Synced traitlets


| Traitlet | Type | Notes |
| --- | --- | --- |
| `selection` | `dict` | Current box or lasso in data coordinates, or `{}`. |
| `selected_count` | `int` | Exact number of selected rows (after filters). |
| `filters` | `list[dict]` | Keep/Exclude steps: `{"action": "keep" \\| "exclude", "selection": {...}}`. |
| `mode` | `str` | Selection tool: `"box"` or `"lasso"`. |
| `layout` | `str` | `"full"` matrix or `"lower"` triangle. |
| `columns` | `list[str]` | Dimension names. |
| `categories` | `list[str]` | Color categories, in sorted order. |
| `palette` | `list[str]` | One CSS color per category. |
| `cell_size` | `int \\| None` | Cell size in pixels; `None` fits the available width. |
| `point_size` | `float \\| None` | Point diameter in pixels; `None` adapts to density. |
| `max_marks` | `int` | Budget of points drawn across all cells. |
| `status` | `str` | `"loading"`, `"ready"` or `"error"`. |
| `error` | `str` | Error message from the browser, if any. |
