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)
/home/docs/.local/share/hatch/env/virtual/hv-anndata/STk7F69l/docs/lib/python3.13/site-packages/numba/cpython/hashing.py:477: UserWarning: FNV hashing is not implemented in Numba. See PEP 456 https://www.python.org/dev/peps/pep-0456/ for rationale over not using FNV. Numba will continue to work, but hashes for built in types will be computed using siphash24. This will permit e.g. dictionaries to continue to behave as expected, however anything relying on the value of the hash opposed to hash as a derived property is likely to not work as expected.
  warnings.warn(msg)

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",
)

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:1491, in Dimensioned._repr_mimebundle_(self, include, exclude)
   1484 def _repr_mimebundle_(self, include=None, exclude=None):
   1485     """Resolves the class hierarchy for the class rendering the
   1486     object using any display hooks registered on Store.display
   1487     hooks.  The output of all registered display_hooks is then
   1488     combined and returned.
   1489 
   1490     """
-> 1491     return Store.render(self)

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

File ~/.local/share/hatch/env/virtual/hv-anndata/STk7F69l/docs/lib/python3.13/site-packages/holoviews/ipython/display_hooks.py:330, in pprint_display(obj)
    328 if not ip.display_formatter.formatters["text/plain"].pprint:
    329     return None
--> 330 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:298, in display(obj, raw_output, **kwargs)
    296 elif isinstance(obj, (CompositeOverlay, ViewableElement)):
    297     with option_state(obj):
--> 298         output = element_display(obj)
    299 elif isinstance(obj, (Layout, NdLayout, AdjointLayout)):
    300     with option_state(obj):

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

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

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

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

File ~/.local/share/hatch/env/virtual/hv-anndata/STk7F69l/docs/lib/python3.13/site-packages/holoviews/plotting/renderer.py:457, in Renderer._render_panel(self, plot, embed, comm)
    455 doc = Document()
    456 with config.set(embed=embed):
--> 457     model = plot.layout._render_model(doc, comm)
    458 if embed:
    459     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:77, in BokehRenderer.get_plot(self_or_cls, obj, doc, renderer, **kwargs)
     70 @bothmethod
     71 def get_plot(self_or_cls, obj, doc=None, renderer=None, **kwargs):
     72     """Given a HoloViews Viewable return a corresponding plot instance.
     73     Allows supplying a document attach the plot to, useful when
     74     combining the bokeh model with another plot.
     75 
     76     """
---> 77     plot = super().get_plot(obj, doc, renderer, **kwargs)
     78     if plot.document is None:
     79         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:291, in Renderer.get_plot(self_or_cls, obj, doc, renderer, comm, **kwargs)
    287     defaults = [kd.default for kd in plot.dimensions]
    288     init_key = tuple(
    289         v if d is None else d for v, d in zip(plot.keys[0], defaults, strict=None)
    290     )
--> 291     plot.update(init_key)
    292 else:
    293     plot = obj

File ~/.local/share/hatch/env/virtual/hv-anndata/STk7F69l/docs/lib/python3.13/site-packages/holoviews/plotting/plot.py:1069, in DimensionedPlot.update(self, key)
   1067 def update(self, key):
   1068     if len(self) == 1 and key in (0, self.keys[0]) and not self.drawn:
-> 1069         return self.initialize_plot()
   1070     item = self.__getitem__(key)
   1071     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:3882, in OverlayPlot.initialize_plot(self, ranges, plot, plots)
   3880 if plot and not self.overlaid:
   3881     self._update_plot(key, plot, element)
-> 3882     self._update_ranges(element, ranges)
   3884 panels = []
   3885 subcoord_y_glyph_renderers = []

File ~/.local/share/hatch/env/virtual/hv-anndata/STk7F69l/docs/lib/python3.13/site-packages/holoviews/plotting/bokeh/element.py:1577, in ElementPlot._update_ranges(self, element, ranges)
   1574 y_range = self.handles["y_range"]
   1575 plot = self.handles["plot"]
-> 1577 self._update_main_ranges(element, x_range, y_range, ranges)
   1579 if self._subcoord_overlaid:
   1580     return

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

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

File ~/.local/share/hatch/env/virtual/hv-anndata/STk7F69l/docs/lib/python3.13/site-packages/holoviews/plotting/bokeh/element.py:2048, in ElementPlot._get_factors(self, element, ranges)
   2046 def _get_factors(self, element, ranges):
   2047     """Get factors for categorical axes."""
-> 2048     xdim, ydim = element.dimensions()[:2]
   2049     xvals = self._get_dimension_factors(element, ranges, xdim)
   2050     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:  # ruff:ignore[blind-except]
    import traceback

    traceback.print_exc()

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?  # ruff:ignore[missing-todo-link]

    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.RasterPlot01191: 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.