Visualizing tools’ results

graph stuff:

  • https://holoviews.org/gallery/demos/bokeh/network_graph.html

  • https://holoviews.org/reference/elements/bokeh/Graph.html

%load_ext autoreload
%autoreload 2
import holoviews as hv

from hv_anndata import A, register
from hv_anndata import scanpy as hv_sc

register()

hv.extension("bokeh")
import numpy as np
import scanpy as sc
from anndata import AnnData
adata = sc.datasets.pbmc68k_reduced()

Embeddings

sc.pp.pca(adata)
sc.pp.neighbors(adata)
sc.tl.umap(adata)

scanpy.pl.embedding() (i.e. scanpy.pl.pca(), scanpy.pl.umap(), scanpy.pl.diffmap(), scanpy.pl.tsne(), scanpy.pl.draw_graph())

missing:

  • add_outline

  • legend_position doesn’t work for graphs

missing convenience:

  • groups to restrict to a subset easily

# without edges, see `scatter` in Basic notebook

hv_sc.draw_graph(adata, A.obsm["X_umap"], "distances", [A.obs["bulk_labels"]]).opts(
    node_color=A.obs["bulk_labels"],
    node_cmap="tab10",
    aspect="square",
    show_legend=True,
    legend_position="right",
)
/home/docs/.local/share/hatch/env/virtual/hv-anndata/STk7F69l/docs/lib/python3.13/site-packages/holoviews/element/graphs.py:337: FutureWarning: Probably comparing to a dimension created from `Dimension.name`. This will not be supported in the future, please report as an issue. 
  if self.nodes and dimension in self.nodes.dimensions():

scanpy.pl.ranking() (scanpy.pl.pca_loadings(), scanpy.pl.pca_variance_ratio())

missing:

  • xoffset/yoffset taken from dimension or text_xoffset/text_yoffset available for Labels: https://github.com/holoviz/holoviews/issues/3884

hv.Layout([
    hv_sc.ranking(adata, A.varm["PCs"][0]).opts(aspect=1.2),
    hv_sc.ranking(adata, A.varm["PCs"][0], include_lowest=False).opts(aspect=0.6),
]).opts(shared_axes=False)

scanpy.pl.pca_overview() is just a layout of multiple pca_ plots

scanpy.pl.embedding_density()

TODO: maybe better approach: https://holoviews.org/gallery/demos/bokeh/iris_density_grid.html

sc.tl.embedding_density(adata, basis="umap", groupby="phase")
# sc.pl.embedding_density(adata, "umap", groupby="phase")
hv_sc.embedding_density(adata, A.obsm["X_umap"], groupby="phase")

Pseudotime and clustering

adata.uns["iroot"] = np.flatnonzero(adata.obs["bulk_labels"] == "CD56+ NK")[0]
sc.tl.diffmap(adata)
sc.tl.dpt(adata, n_branchings=1)

scanpy.pl.dpt_timeseries() (as_heatmap=True)

missing:

  • figure out why the weird extra vdim is needed

markers: list[str] = ["C1QA", "PSAP", "CD79A", "CD79B", "CST3", "LYZ"]
display(
    hv.HeatMap(adata[:, markers], [A.obs.index, A.var.index], [A.X[:, :]])
    * hv.VLines(adata.uns["dpt_changepoints"])
)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
File ~/.local/share/hatch/env/virtual/hv-anndata/STk7F69l/docs/lib/python3.13/site-packages/holoviews/core/dimension.py:1381, in Dimensioned._repr_mimebundle_(self, include, exclude)
   1374 def _repr_mimebundle_(self, include=None, exclude=None):
   1375     """Resolves the class hierarchy for the class rendering the
   1376     object using any display hooks registered on Store.display
   1377     hooks.  The output of all registered display_hooks is then
   1378     combined and returned.
   1379 
   1380     """
-> 1381     return Store.render(self)

File ~/.local/share/hatch/env/virtual/hv-anndata/STk7F69l/docs/lib/python3.13/site-packages/holoviews/core/options.py:1433, in Store.render(cls, obj)
   1431 data, metadata = {}, {}
   1432 for hook in hooks:
-> 1433     ret = hook(obj)
   1434     if ret is None:
   1435         continue

File ~/.local/share/hatch/env/virtual/hv-anndata/STk7F69l/docs/lib/python3.13/site-packages/holoviews/ipython/display_hooks.py:340, in pprint_display(obj)
    338 if not ip.display_formatter.formatters['text/plain'].pprint:
    339     return None
--> 340 return display(obj, raw_output=True)

File ~/.local/share/hatch/env/virtual/hv-anndata/STk7F69l/docs/lib/python3.13/site-packages/holoviews/ipython/display_hooks.py:308, in display(obj, raw_output, **kwargs)
    306 elif isinstance(obj, (CompositeOverlay, ViewableElement)):
    307     with option_state(obj):
--> 308         output = element_display(obj)
    309 elif isinstance(obj, (Layout, NdLayout, AdjointLayout)):
    310     with option_state(obj):

File ~/.local/share/hatch/env/virtual/hv-anndata/STk7F69l/docs/lib/python3.13/site-packages/holoviews/ipython/display_hooks.py:205, in display_hook.<locals>.wrapped(element)
    203 try:
    204     max_frames = OutputSettings.options['max_frames']
--> 205     mimebundle = fn(element, max_frames=max_frames)
    206     if mimebundle is None:
    207         return {}, {}

File ~/.local/share/hatch/env/virtual/hv-anndata/STk7F69l/docs/lib/python3.13/site-packages/holoviews/ipython/display_hooks.py:246, in element_display(element, max_frames)
    243 if type(element) not in Store.registry[backend]:
    244     return None
--> 246 return render(element)

File ~/.local/share/hatch/env/virtual/hv-anndata/STk7F69l/docs/lib/python3.13/site-packages/holoviews/ipython/display_hooks.py:76, in render(obj, **kwargs)
     73 if renderer.fig == 'pdf':
     74     renderer = renderer.instance(fig='png')
---> 76 return renderer.components(obj, **kwargs)

File ~/.local/share/hatch/env/virtual/hv-anndata/STk7F69l/docs/lib/python3.13/site-packages/holoviews/plotting/renderer.py:396, in Renderer.components(self, obj, fmt, comm, **kwargs)
    394 embed = (not (dynamic or streams or self.widget_mode == 'live') or config.embed)
    395 if embed or config.comms == 'default':
--> 396     return self._render_panel(plot, embed, comm)
    397 return self._render_ipywidget(plot)

File ~/.local/share/hatch/env/virtual/hv-anndata/STk7F69l/docs/lib/python3.13/site-packages/holoviews/plotting/renderer.py:403, in Renderer._render_panel(self, plot, embed, comm)
    401 doc = Document()
    402 with config.set(embed=embed):
--> 403     model = plot.layout._render_model(doc, comm)
    404 if embed:
    405     return render_model(model, comm)

File ~/.local/share/hatch/env/virtual/hv-anndata/STk7F69l/docs/lib/python3.13/site-packages/panel/viewable.py:775, in Viewable._render_model(self, doc, comm)
    773 if comm is None:
    774     comm = state._comm_manager.get_server_comm()
--> 775 model = self.get_root(doc, comm)
    777 if self._design and self._design.theme.bokeh_theme:
    778     doc.theme = self._design.theme.bokeh_theme

File ~/.local/share/hatch/env/virtual/hv-anndata/STk7F69l/docs/lib/python3.13/site-packages/panel/layout/base.py:332, in Panel.get_root(self, doc, comm, preprocess)
    328 def get_root(
    329     self, doc: Document | None = None, comm: Comm | None = None,
    330     preprocess: bool = True
    331 ) -> Model:
--> 332     root = super().get_root(doc, comm, preprocess)
    333     # ALERT: Find a better way to handle this
    334     if hasattr(root, 'styles') and 'overflow-x' in root.styles:

File ~/.local/share/hatch/env/virtual/hv-anndata/STk7F69l/docs/lib/python3.13/site-packages/panel/viewable.py:705, in Renderable.get_root(self, doc, comm, preprocess)
    703 wrapper = self._design._wrapper(self)
    704 if wrapper is self:
--> 705     root = self._get_model(doc, comm=comm)
    706     if preprocess:
    707         self._preprocess(root)

File ~/.local/share/hatch/env/virtual/hv-anndata/STk7F69l/docs/lib/python3.13/site-packages/panel/layout/base.py:316, in Panel._get_model(self, doc, root, parent, comm)
    314 root = root or model
    315 self._models[root.ref['id']] = (model, parent)
--> 316 objects, _ = self._get_objects(model, [], doc, root, comm)
    317 props = self._get_properties(doc)
    318 props[self._property_mapping['objects']] = objects

File ~/.local/share/hatch/env/virtual/hv-anndata/STk7F69l/docs/lib/python3.13/site-packages/panel/layout/base.py:298, in Panel._get_objects(self, model, old_objects, doc, root, comm)
    296 else:
    297     try:
--> 298         child = pane._get_model(doc, root, model, comm)
    299     except RerenderError as e:
    300         if e.layout is not None and e.layout is not self:

File ~/.local/share/hatch/env/virtual/hv-anndata/STk7F69l/docs/lib/python3.13/site-packages/panel/pane/holoviews.py:494, in HoloViews._get_model(self, doc, root, parent, comm)
    492     plot = self.object
    493 else:
--> 494     plot = self._render(doc, comm, root)
    496 plot.pane = self
    497 backend = plot.renderer.backend

File ~/.local/share/hatch/env/virtual/hv-anndata/STk7F69l/docs/lib/python3.13/site-packages/panel/pane/holoviews.py:588, in HoloViews._render(self, doc, comm, root)
    585     if comm:
    586         kwargs['comm'] = comm
--> 588 return renderer.get_plot(self.object, **kwargs)

File ~/.local/share/hatch/env/virtual/hv-anndata/STk7F69l/docs/lib/python3.13/site-packages/holoviews/plotting/bokeh/renderer.py:70, in BokehRenderer.get_plot(self_or_cls, obj, doc, renderer, **kwargs)
     63 @bothmethod
     64 def get_plot(self_or_cls, obj, doc=None, renderer=None, **kwargs):
     65     """Given a HoloViews Viewable return a corresponding plot instance.
     66     Allows supplying a document attach the plot to, useful when
     67     combining the bokeh model with another plot.
     68 
     69     """
---> 70     plot = super().get_plot(obj, doc, renderer, **kwargs)
     71     if plot.document is None:
     72         plot.document = Document() if self_or_cls.notebook_context else curdoc()

File ~/.local/share/hatch/env/virtual/hv-anndata/STk7F69l/docs/lib/python3.13/site-packages/holoviews/plotting/renderer.py:239, in Renderer.get_plot(self_or_cls, obj, doc, renderer, comm, **kwargs)
    236     defaults = [kd.default for kd in plot.dimensions]
    237     init_key = tuple(v if d is None else d for v, d in
    238                      zip(plot.keys[0], defaults, strict=None))
--> 239     plot.update(init_key)
    240 else:
    241     plot = obj

File ~/.local/share/hatch/env/virtual/hv-anndata/STk7F69l/docs/lib/python3.13/site-packages/holoviews/plotting/plot.py:958, in DimensionedPlot.update(self, key)
    956 def update(self, key):
    957     if len(self) == 1 and key in (0, self.keys[0]) and not self.drawn:
--> 958         return self.initialize_plot()
    959     item = self.__getitem__(key)
    960     self.traverse(lambda x: setattr(x, '_updated', True))

File ~/.local/share/hatch/env/virtual/hv-anndata/STk7F69l/docs/lib/python3.13/site-packages/holoviews/plotting/bokeh/element.py:3466, in OverlayPlot.initialize_plot(self, ranges, plot, plots)
   3464 if plot and not self.overlaid:
   3465     self._update_plot(key, plot, element)
-> 3466     self._update_ranges(element, ranges)
   3468 panels = []
   3469 subcoord_y_glyph_renderers = []

File ~/.local/share/hatch/env/virtual/hv-anndata/STk7F69l/docs/lib/python3.13/site-packages/holoviews/plotting/bokeh/element.py:1342, in ElementPlot._update_ranges(self, element, ranges)
   1339 y_range = self.handles['y_range']
   1340 plot = self.handles['plot']
-> 1342 self._update_main_ranges(element, x_range, y_range, ranges)
   1344 if self._subcoord_overlaid:
   1345     return

File ~/.local/share/hatch/env/virtual/hv-anndata/STk7F69l/docs/lib/python3.13/site-packages/holoviews/plotting/bokeh/element.py:1385, in ElementPlot._update_main_ranges(self, element, x_range, y_range, ranges, subcoord)
   1383 xfactors, yfactors = None, None
   1384 if any(isinstance(ax_range, FactorRange) for ax_range in [x_range, y_range]):
-> 1385     xfactors, yfactors = self._get_factors(element, ranges)
   1386 framewise = self.framewise
   1387 streaming = (self.streaming and any(stream._triggering and stream.following
   1388                                     for stream in self.streaming))

File ~/.local/share/hatch/env/virtual/hv-anndata/STk7F69l/docs/lib/python3.13/site-packages/holoviews/plotting/bokeh/element.py:3417, in OverlayPlot._get_factors(self, overlay, ranges)
   3415 if el is not None:
   3416     elranges = util.match_spec(el, ranges)
-> 3417     xfs, yfs = sp._get_factors(el, elranges)
   3418     if len(xfs):
   3419         xfactors.append(xfs)

File ~/.local/share/hatch/env/virtual/hv-anndata/STk7F69l/docs/lib/python3.13/site-packages/holoviews/plotting/bokeh/element.py:1770, in ElementPlot._get_factors(self, element, ranges)
   1766 def _get_factors(self, element, ranges):
   1767     """Get factors for categorical axes.
   1768 
   1769     """
-> 1770     xdim, ydim = element.dimensions()[:2]
   1771     xvals = self._get_dimension_factors(element, ranges, xdim)
   1772     yvals = self._get_dimension_factors(element, ranges, ydim)

ValueError: not enough values to unpack (expected 2, got 1)
:Overlay
   .HeatMap.I :HeatMap   [A.obs.index,A.var.index]   (A.X[:, :])
   .VLines.I  :VLines   [x]
markers: list[str] = ["C1QA", "PSAP", "CD79A", "CD79B", "CST3", "LYZ"]
hv.HeatMap(
    adata[adata.obs["dpt_order_indices"], markers],
    [A.obs.index, A.var.index],
    [A.X[:, :]],
).opts(xticks=0, colorbar=True, width=400, height=200) * hv.VLines(
    (adata.uns["dpt_changepoints"], [0] * len(adata.uns["dpt_changepoints"])),
    vdims=["?"],
)

scanpy.pl.dpt_timeseries() (as_heatmap=False)

def dpt_timeseries(adata: AnnData):
    adata = adata[adata.obs["dpt_order_indices"]].copy()
    return (
        hv.Overlay([
            hv.Points(adata, [A.obs.index, A.X[:, gene]], label=gene)
            for gene in adata.var_names
        ])
        * hv.VLines(
            (
                adata.uns["dpt_changepoints"],
                [0] * len(adata.uns["dpt_changepoints"]),
            ),
            vdims=["?"],
        )
    ).opts(xticks=0)


markers = ["C1QA", "PSAP", "CD79A", "CD79B", "CST3", "LYZ"]
dpt_timeseries(adata[:, markers]).opts(width=800, legend_position="right")

scanpy.pl.dpt_groups_pseudotime()

Skip for now, since it’s broken in scanpy: https://github.com/scverse/scanpy/issues/3086

try:
    sc.pl.dpt_groups_pseudotime(adata)
except Exception:  # noqa: BLE001
    import traceback

    traceback.print_exc()
../../_images/95866451e99d144a4339cb33a9f74d009bb31225419c77664bf07cd32c9876aa.png

scanpy.pl.correlation_matrix()

missing:

  • again: dendrogram on heatmaps

  • invalid warning:

    “WARNING:param.RasterPlot02026: aspect value was ignored because absolute width and height values were provided. Either supply explicit frame_width and frame_height to achieve desired aspect OR supply a combination of width or height and an aspect value.”

sc.tl.dendrogram(adata, "bulk_labels")
# sc.pl.correlation_matrix(adata, groupby="bulk_labels")
def correlation_matrix(adata: AnnData, groupby: str):
    # from scipy.sparse import coo_array

    dendrogram_key = f"dendrogram_{groupby}"  # _get_dendrogram_key(...)
    mat = adata.uns[dendrogram_key]["correlation_matrix"]
    index = adata.uns[dendrogram_key]["categories_idx_ordered"]
    mat = mat[index[::-1], :][:, index]  # TODO: why do we have to flip?  # noqa: TD003

    labels = list(adata.obs[groupby].cat.categories)
    labels = np.array(labels).astype("str")[index]
    ticks = list(enumerate(labels))

    return hv.Image(
        mat, bounds=(-0.5, -0.5, len(labels) - 0.5, len(labels) - 0.5)
    ).opts(
        xticks=ticks,
        yticks=ticks,
        xrotation=45,
        frame_width=400,
        frame_height=400,
        aspect=1,
    )


correlation_matrix(adata, "bulk_labels")
WARNING:param.RasterPlot01200: aspect value was ignored because absolute width and height values were provided. Either supply explicit frame_width and frame_height to achieve desired aspect OR supply a combination of width or height and an aspect value.

Marker genes

scanpy.pl.rank_genes_groups(), scanpy.pl.rank_genes_groups_dotplot(), scanpy.pl.rank_genes_groups_heatmap(), scanpy.pl.rank_genes_groups_matrixplot(), scanpy.pl.rank_genes_groups_tracksplot(), scanpy.pl.rank_genes_groups_stacked_violin(), scanpy.pl.rank_genes_groups_violin()

are all just pre-parametrized versions of other plots; rank_genes_groups is ranking, the others are just the suffix.