Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 51 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,55 @@ change. Versions absent from the list were never released.
## [Unreleased]

### Breaking
- `annotated_heatmap` returns one `PlotSpec` instead of a `gggrid` of
`SupPlotsSpec` panels. Annotation tracks are ggside side layers
(`geom_xsidetile`/`geom_ysidetile`), each with its own fill scale
(`new_scale_fill`) and legend, so they zoom and pan with the heatmap in HTML
output (`interactive=True`). Track titles beside the bands are hidden by
default, since the legends name each track; the new `track_titles=True`
shows them (a top or bottom band's names on its right, a left or right
band's names above it). The group dendrogram is drawn in
the observation band, outermost. `layers_all` acts as `layers` now that there
is a single plot. `column_annotation_height`, `row_annotation_width` and the
tree's 0.15 width set the bands' share of the panel
(`ggside_panel_scale_x`/`_y`).
- Migration: index the plot's `as_dict()["layers"]` instead of
`["figures"]`; combine it with other plots through `gggrid` as any
`PlotSpec`.
- ggrust 0.6 is required. `cellestial.setup_html` is removed, with the ggrust
`LetsPlot` shim it aliased; it was a no-op, as is the setup call cellestial
made on import in IPython.
- Migration: delete `setup_html()` calls; use `ggrust.display("html" | "svg" | "png")`
to choose notebook output.
- `dotplot`, `stacked_violin`, `matrixplot` and `heatmap(aggregate=True)` draw
the group dendrogram with ggrust's axis dendrogram (`scale_y_dendro` +
`guide_axis_dendro`) instead of a `geom_path` layer. The tree sits in a fixed
band right of the panel rather than inside an extended x range, so the data
area no longer shrinks to make room for it. `dendrogram_kwargs` is removed from
these functions and from `heatmap`; the per-cell `heatmap(aggregate=False)`
keeps its path layer, styled by `dendrogram_color` and `dendrogram_size`.
- Migration: drop `dendrogram_kwargs`; style the tree with `dendrogram_color`
and `dendrogram_size`, or with `theme(axis_ticks_y=element_line(...))`.
- `arrow_axis` returns a ggrust theme with arrow axis lines instead of a deferred
layer of `geom_path`/`geom_segment`. It no longer takes `plot=` (the theme needs
no data) or `**geom_kwargs`; `linetype=` is now an explicit parameter. `length`
is a fraction of the panel rather than of the data range, so arrows start at the
panel corner.
- Migration: drop `plot=`; pass `linetype=` directly; other geom kwargs such as
`alpha` are no longer supported.
- `bracket` computes its tests with ggrust's `stat_compare_means` instead of
scipy. P-values, the `none`/`bonferroni`/`fdr_bh` corrections and the stars
agree for groups of 50 or more values; below that, tie-free groups get R's
exact Wilcoxon test, so `mannwhitney` p-values change there. Stars now use
`<=` cutoffs, and p-value labels show 2 significant digits. Bracket tips are
3% of the y range (ggpubr's default) instead of a fixed size. Removed:
`test="student"`, `alternative`, `label="padj"` and label sequences,
`label_format`, `prefix`, `prefix_style`, `separator` and `threshold`.
- Migration: `label="padj"` becomes `label="pvalue"` (it shows the adjusted
p-value); `threshold=0.05` becomes `hide_ns=True` with `label="stars"`;
pass `tip_length=` for other tips. One-sided and pooled-variance tests are
no longer available: ggrust's `student` is Welch's test, so it is not offered
under that name; use `test="ttest"`.
- `spatialdata` is no longer installed by default. It is only needed to plot
`SpatialData` objects with `spatial`, `spatials` or `build_frame`; AnnData
inputs, spatial ones included, work without it. Dropping it from the core
Expand All @@ -23,7 +72,8 @@ change. Versions absent from the list were never released.
in code that adds layers, scales or themes to cellestial plots.
- `bracket(test="ttest")` is now Welch's t-test rather than the pooled-variance
one, matching R's `t.test()` and scanpy. P-values change.
- Migration: pass `test="student"`, a new option, for the previous behaviour.
- Migration: none; the pooled-variance test is no longer available (see the
`stat_compare_means` entry above).
- `volcano` and `volcanos` name their p-value column after the values it holds.
Under the default `use_adjusted_pvalue=True` the column, tooltip entry and
y-axis title read `pvalue_adj` and `-log10(Padj)`; raw p-values are unaffected.
Expand Down
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,6 @@ cl.boxplot(
) + scale_fill_hue() + cl.bracket(
y_padding=0.2,
label="pvalue",
prefix="p",
prefix_style="<",
comparisons=[
("Lymphocytes", "Monocytes"),
("Monocytes", "Erythroid"),
Expand Down
90 changes: 30 additions & 60 deletions SKILLS/dendrogram.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,83 +2,53 @@

How dendrograms are wired into the heatmap variants (`heatmap`, `matrixplot`, `dotplot`, `stacked_violin`). Use this as the template when adding a dendrogram to a new plot.

## Helpers

Two private helpers live in [cellestial/util/dendrogram.py](../cellestial/util/dendrogram.py) and are re-exported from [cellestial.util](../cellestial/util/__init__.py):
## Two cases

- `_get_dendrogram(data, group_by) -> (categories_ordered, paths)`
- Reads `data.uns[f"dendrogram_{group_by}"]`, computes via `scanpy.tl.dendrogram` if missing.
- `categories_ordered`: list of group labels in dendrogram leaf order. **Use this as the y order.**
- `paths`: Polars DataFrame with normalized `x` (leaf positions 0..n_groups-1) and `y` (dcoord scaled to 0..1), plus `group` id per linkage segment.
- **One row per group** (dotplot, stacked_violin, matrixplot, heatmap with `aggregate=True`): use ggrust's native axis dendrogram. The tree orders the y-axis and draws itself right of the panel.
- **Per-cell rows** (heatmap with `aggregate=False`; `complex/annotated_heatmap.py`): groups span many rows of unequal height, so a discrete dendrogram scale cannot express them. These keep the hand-drawn tree: `geom_path` in the heatmap, two-point `geom_xsideline`/`geom_ysideline` segments in the annotated heatmap's observation side band (outermost, beyond the tracks).

- `_get_dendrogram_path_frame(paths, *, n_x, n_groups, group_centers, dendrogram_ratio=0.15) -> pl.DataFrame`
- Maps the normalized paths into actual plot coordinates on the **right side**, along the y-axis.
- Width of dendrogram = `n_x * dendrogram_ratio`, anchored at `x = n_x - 0.5`.
- `group_centers` is the y-pixel center of each group, ordered to match `categories_ordered`.
## Helpers

## Required parameters to expose
All live in [cellestial/util/dendrogram.py](../cellestial/util/dendrogram.py), re-exported from [cellestial.util](../cellestial/util/__init__.py). Each reads `data.uns[use_key or f"dendrogram_{group_by}"]` and computes it with `scanpy.tl.dendrogram` if missing.

```python
dendrogram: bool = False,
dendrogram_color: str = "black",
dendrogram_size: float = 0.5,
dendrogram_kwargs: dict | None = None,
```
- `_get_dendrogram_tree(data, group_by, *, use_key=None) -> ClusterTree`: scanpy's linkage as a ggrust tree; `tree.ordered_labels` is scanpy's `categories_ordered`.
- `_dendrogram_y_axis(tree, *, limits, fixed=False)`: `scale_y_dendro(..., guide=guide_axis_dendro(side="opposite"))` plus a coord holding the y `limits`. Pass `fixed=True` for `geom_raster` plots, which default to `coord_fixed()`.
- `_dendrogram_line_theme(*, color, size)`: the tree is drawn with the y tick line, so this styles `axis_ticks_y` (size in geom units) and sets the tick length to 0. Add it **after** any full theme (`theme_classic()` in `_THEME_DOTPLOT` resets it).
- Per-cell only: `_get_dendrogram(...) -> (categories_ordered, paths)` and `_get_dendrogram_path_frame(paths, *, n_x, n_groups, group_centers)`, which map scipy's `icoord`/`dcoord` beside the panel.

## Wiring (three steps)
## Wiring a one-row-per-group plot

**1. Determine y order before building positions.** The dendrogram dictates group order; everything else (positions, separator lines, group bars) must follow.
Keep numeric `position_y` values (group `i` of the tree order at `i`): numeric layers such as key-group brackets and the border `geom_rect` still work on the discrete dendrogram scale.

```python
if dendrogram:
y_order_groups, paths = _get_dendrogram(data, group_by)
tree = _get_dendrogram_tree(data, group_by, use_key=dendrogram_key)
y_order_groups = list(tree.ordered_labels)
...
if dendrogram:
plot += _dendrogram_y_axis(tree, limits=[-0.5, y_max_limit])
else:
y_order_groups = frame.select(group_by).unique(maintain_order=True)[group_by].cast(pl.String).to_list()
paths = None
```

**2. After computing layout, build the path frame and add `geom_path`.** You need `n_x`, `n_groups`, and `group_centers` (the y-coordinate of each group's center, in `y_order_groups` order).

```python
plot += scale_y_continuous(breaks=list(range(n_y)), labels=y_order_groups,
limits=[-0.5, y_max_limit], expand=[0, 0])
...
plot += _THEME_DOTPLOT
if dendrogram:
dendro_frame = _get_dendrogram_path_frame(
paths, n_x=n_x, n_groups=len(y_order_groups), group_centers=group_centers,
)
plot += geom_path(
data=dendro_frame,
mapping=aes(x="x", y="y", group="group"),
color=dendrogram_color,
size=dendrogram_size,
**(dendrogram_kwargs or {}),
)
plot += _dendrogram_line_theme(color=dendrogram_color, size=dendrogram_size)
```

**3. Extend the x-axis limit to include the dendrogram width.** The dendrogram lives at `x in [n_x - 0.5, n_x - 0.5 + n_x*dendrogram_ratio]`. If you set explicit `scale_x_continuous(limits=...)` or have a bordering `geom_rect`, account for this:
No x limit needs to make room for the tree; it lives in its own band outside the panel.

## Parameters to expose

```python
x_max_limit = n_x - 0.5
if dendrogram:
x_max_limit = dendro_frame["x"].max()
plot += scale_x_continuous(breaks=list(range(n_x)), labels=x_keys, limits=[-0.5, x_max_limit], expand=[0, 0])
dendrogram: bool = False,
dendrogram_color: str = "black",
dendrogram_size: float = 0.5,
dendrogram_key: str | None = None,
```

If a border rectangle frames the data area, draw it from `-0.5` to `n_x - 0.5` so the dendrogram sits **outside** the frame (see dotplot pattern).

## Axis type

The plot must use **continuous numeric** axes (not discrete), because the dendrogram lives at fractional x-coordinates beyond the data area. Pattern:

- Encode positions as floats (`position_x`, `position_y`).
- Use `scale_x_continuous(breaks=list(range(n_x)), labels=x_keys)` and analogous y scale to fake categorical labels on a continuous axis.
- Always pass `limits=[...]` and `expand=[0, 0]` when a dendrogram is present, so it isn't clipped.

## Computing `group_centers`

- **Aggregated row-per-group plots** (matrixplot, dotplot, stacked_violin, heatmap with `aggregate=True`): `group_centers = [float(i) for i in range(n_groups)]`.
- **Per-cell rows** (heatmap with `aggregate=False`): compute the mean `position_y` of cells in each group, sorted by group order. See `_assign_positions` in [heatmap.py](../cellestial/single/heatmap/heatmap.py) for the canonical implementation.

## Reference implementations

- [cellestial/single/heatmap/heatmap.py](../cellestial/single/heatmap/heatmap.py) (per-cell and aggregated)
- [cellestial/single/heatmap/dotplot.py](../cellestial/single/heatmap/dotplot.py) (with bordering rectangle)
- [cellestial/single/heatmap/dotplot.py](../cellestial/single/heatmap/dotplot.py) (with bordering rectangle and key-group brackets)
- [cellestial/single/heatmap/stacked_violin.py](../cellestial/single/heatmap/stacked_violin.py)
- [cellestial/single/heatmap/heatmap.py](../cellestial/single/heatmap/heatmap.py) (native when aggregated, path for per-cell rows)
17 changes: 0 additions & 17 deletions cellestial/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
__lazy_modules__ = ["ggrust"]

from ggrust import LetsPlot

from cellestial import datasets
from cellestial._version import __version__, versions
from cellestial.complex import annotated_heatmap
Expand Down Expand Up @@ -65,8 +61,6 @@
show_colors,
)

setup_html = LetsPlot.setup_html

__all__ = [
"BLUE",
"CHERRY",
Expand Down Expand Up @@ -115,7 +109,6 @@
"ridges",
"save",
"scatter",
"setup_html",
"show_colors",
"spatial",
"spatials",
Expand All @@ -133,13 +126,3 @@
"xyplot",
"xyplots",
]

try:
from IPython.core.interactiveshell import InteractiveShell

if InteractiveShell.initialized():
from ggrust import LetsPlot

LetsPlot.setup_html()
except ImportError:
pass
Loading
Loading