Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
cb9fee8
expose html rep building blocks
katosh Dec 3, 2025
efc7897
inlcude SpatialData example
katosh Dec 3, 2025
ff56397
illustrate customization in SpatialData example
katosh Dec 3, 2025
a5e44d5
custom sections for SpatialData
katosh Dec 3, 2025
047b060
customizable meta column
katosh Dec 6, 2025
d0d009e
simplify/explain SpatialData custom html rep
katosh Dec 6, 2025
e8cf0cd
centralize mure UI elements
katosh Dec 7, 2025
611809e
improve nested search
katosh Dec 7, 2025
628e9c4
regex and case sensitive search
katosh Dec 7, 2025
86e9afe
merge html_rep
katosh Dec 7, 2025
a920208
SpatialData exmaple coordinate systems
katosh Dec 11, 2025
5ba035e
Merge remote-tracking branch 'origin/main' into expose_html_rep
katosh Dec 15, 2025
7ee414a
improve .raw render in `_rep_html_`
katosh Dec 15, 2025
e6f261f
better error handling for html rep
katosh Dec 15, 2025
42fa262
Merge html_rep to sync main merge
katosh Dec 15, 2025
2fbd1db
multi section SectionFormatter & do not list fomratted sections in "o…
katosh Dec 15, 2025
55c6f50
document "Building Custom _repr_html_"
katosh Dec 15, 2025
2fc88e8
test html rep public API
katosh Dec 15, 2025
9bd4ae0
test serializability for columns of .obs and .var
katosh Dec 19, 2025
d47fd5b
test serializability of column names and keys
katosh Dec 19, 2025
181430c
also warn datetime non-serializability
katosh Dec 19, 2025
61e49ee
maintainer info in case of test failure
katosh Dec 19, 2025
29b5caa
futureproof more serializationw warnings
katosh Dec 19, 2025
2597d99
coding style: relative imports and no section headers
katosh Dec 20, 2025
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
8 changes: 6 additions & 2 deletions src/anndata/_core/anndata.py
Original file line number Diff line number Diff line change
Expand Up @@ -599,9 +599,13 @@ def _repr_html_(self) -> str | None:
from anndata._repr import generate_repr_html

return generate_repr_html(self)
except Exception: # noqa: BLE001
except Exception as e: # noqa: BLE001
# Intentional broad catch: HTML repr should never crash the notebook
# Fall back to text repr if HTML generation fails
# Fall back to text repr if HTML generation fails, but log the error
warn(
f"HTML repr failed, falling back to text repr: {e}",
UserWarning,
)
return None

def __eq__(self, other):
Expand Down
168 changes: 165 additions & 3 deletions src/anndata/_repr/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,128 @@ def get_entries(self, obj, context):
)
for k, v in obj.obst.items()
]

Building Custom _repr_html_
---------------------------
For packages with AnnData-adjacent objects (like SpatialData, MuData) that need
their own ``_repr_html_``, you can reuse anndata's CSS, JavaScript, and helpers.

**Basic structure**::

from anndata._repr import get_css, get_javascript


class MyData:
def _repr_html_(self):
container_id = f"mydata-{id(self)}"
return f'''
{get_css()}
<div class="anndata-repr" id="{container_id}">
<div class="anndata-hdr">
<span class="adata-type">MyData</span>
<span class="adata-shape">100 items</span>
</div>
<div class="adata-sections">
<!-- sections go here -->
</div>
</div>
{get_javascript(container_id)}
'''

**CSS classes** (stable, can be used directly):

- ``anndata-repr``: Main container (required for JS and styling)
- ``anndata-hdr``: Header row (flexbox, contains type/shape/badges)
- ``anndata-ftr``: Footer row (version, memory info)
- ``adata-type``: Type name span in header
- ``adata-shape``: Shape/dimensions span in header
- ``adata-sections``: Container for all sections
- ``adata-section``: Individual section wrapper
- ``adata-section-header``: Section header (clickable to fold)
- ``adata-section-content``: Section content (rows)

**CSS variables** (set on ``.anndata-repr`` element):

- ``--anndata-name-col-width``: Width of name column (default: 150px)
- ``--anndata-type-col-width``: Width of type column (default: 220px)

**Using render helpers** for consistent section rendering::

from anndata._repr import (
get_css,
get_javascript,
render_section,
render_formatted_entry,
render_badge,
render_search_box,
FormattedEntry,
FormattedOutput,
)


def _repr_html_(self):
container_id = f"mydata-{id(self)}"
parts = [get_css()]

# Header
parts.append(f'''
<div class="anndata-repr" id="{container_id}">
<div class="anndata-hdr">
<span class="adata-type">MyData</span>
{render_badge("Zarr", "adata-badge-backed")}
<span style="flex-grow:1;"></span>
{render_search_box(container_id)}
</div>
<div class="adata-sections">
''')

# Build section entries
entries = []
for key, value in self.items.items():
entry = FormattedEntry(
key=key,
output=FormattedOutput(
type_name=f"array {value.shape}",
css_class="dtype-ndarray",
),
)
entries.append(render_formatted_entry(entry))

# Render section
parts.append(
render_section(
"items",
"\\n".join(entries),
n_items=len(self.items),
)
)

parts.append("</div></div>")
parts.append(get_javascript(container_id))
return "\\n".join(parts)

**Embedding nested AnnData** with full interactivity::

from anndata._repr import generate_repr_html, FormattedEntry, FormattedOutput

nested_html = generate_repr_html(adata, depth=1, max_depth=3)
entry = FormattedEntry(
key="table",
output=FormattedOutput(
type_name=f"AnnData ({adata.n_obs} x {adata.n_vars})",
html_content=nested_html,
is_expandable=True,
),
)

**Complete example**: See ``MockSpatialData`` in ``tests/visual_inspect_repr_html.py``
for a full implementation with images, labels, points, shapes, and nested tables.
"""

from __future__ import annotations

# Import constants from dedicated module (single source of truth)
from anndata._repr.constants import (
from .constants import (
DEFAULT_FOLD_THRESHOLD,
DEFAULT_MAX_CATEGORIES,
DEFAULT_MAX_DEPTH,
Expand All @@ -115,6 +231,7 @@ def get_entries(self, obj, context):
DEFAULT_PREVIEW_ITEMS,
DEFAULT_TYPE_WIDTH,
DEFAULT_UNIQUE_LIMIT,
NOT_SERIALIZABLE_MSG,
)

# Documentation base URL
Expand All @@ -135,8 +252,29 @@ def get_entries(self, obj, context):
)

# Import main functionality
from anndata._repr.html import generate_repr_html # noqa: E402
from anndata._repr.registry import ( # noqa: E402
# Inline styles for graceful degradation (from single source of truth)
from .constants import STYLE_HIDDEN # noqa: E402

# Building blocks for packages that want to create their own _repr_html_
# These allow reusing anndata's styling while building custom representations
from .css import get_css # noqa: E402

# HTML rendering helpers for building custom sections
# UI component helpers (search box, fold icon, badges, etc.)
from .formatters import check_column_name # noqa: E402
from .html import ( # noqa: E402
generate_repr_html,
render_badge,
render_copy_button,
render_fold_icon,
render_formatted_entry,
render_header_badges,
render_search_box,
render_section,
render_warning_icon,
)
from .javascript import get_javascript # noqa: E402
from .registry import ( # noqa: E402
UNS_TYPE_HINT_KEY,
FormattedEntry,
FormattedOutput,
Expand All @@ -150,6 +288,11 @@ def get_entries(self, obj, context):
formatter_registry,
register_formatter,
)
from .utils import ( # noqa: E402
escape_html,
format_memory_size,
format_number,
)

__all__ = [ # noqa: RUF022 # organized by category, not alphabetically
# Constants
Expand All @@ -164,6 +307,7 @@ def get_entries(self, obj, context):
"DEFAULT_TYPE_WIDTH",
"DOCS_BASE_URL",
"SECTION_ORDER",
"NOT_SERIALIZABLE_MSG",
# Main function
"generate_repr_html",
# Registry for extensibility
Expand All @@ -178,4 +322,22 @@ def get_entries(self, obj, context):
# Type hint extraction (for tagged data in uns)
"extract_uns_type_hint",
"UNS_TYPE_HINT_KEY",
# Building blocks for custom _repr_html_ implementations
"get_css",
"get_javascript",
"escape_html",
"format_number",
"format_memory_size",
"render_section",
"render_formatted_entry",
"STYLE_HIDDEN",
# UI component helpers
"render_search_box",
"render_fold_icon",
"render_copy_button",
"render_badge",
"render_header_badges",
"render_warning_icon",
# Validation helpers
"check_column_name",
]
8 changes: 8 additions & 0 deletions src/anndata/_repr/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,11 @@
# Column widths (pixels)
DEFAULT_MAX_FIELD_WIDTH = 400 # Max width for field name column
DEFAULT_TYPE_WIDTH = 220 # Width for type column

# Inline styles for graceful degradation (hidden until JS enables)
STYLE_HIDDEN = "display:none;"
STYLE_SECTION_CONTENT = "padding:0;overflow:hidden;"
STYLE_SECTION_TABLE = "width:100%;border-collapse:collapse;"

# Warning messages
NOT_SERIALIZABLE_MSG = "Not serializable to H5AD/Zarr"
70 changes: 63 additions & 7 deletions src/anndata/_repr/css.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,23 +357,32 @@ def get_css() -> str:
border-bottom: 1px solid var(--anndata-border-light);
}

.anndata-repr .adata-search-input {
width: 100%;
/* Search box container with inline toggle buttons */
.anndata-repr .adata-search-box {
display: inline-flex;
align-items: center;
max-width: 300px;
padding: 6px 10px;
font-size: 12px;
border: 1px solid var(--anndata-border-color);
border-radius: var(--anndata-radius);
background: var(--anndata-bg-primary);
color: var(--anndata-text-primary);
outline: none;
transition: border-color 0.15s;
}

.anndata-repr .adata-search-input:focus {
.anndata-repr .adata-search-box:focus-within {
border-color: var(--anndata-accent-color);
}

.anndata-repr .adata-search-input {
flex: 1;
min-width: 120px;
padding: 6px 8px;
font-size: 12px;
border: none;
background: transparent;
color: var(--anndata-text-primary);
outline: none;
}

.anndata-repr .adata-search-input::placeholder {
color: var(--anndata-text-muted);
}
Expand All @@ -389,6 +398,53 @@ def get_css() -> str:
display: inline;
}

/* Search toggle buttons (case sensitive, regex) - inside search box */
.anndata-repr .adata-search-toggles {
display: flex;
gap: 1px;
padding-right: 4px;
border-left: 1px solid var(--anndata-border-light);
margin-left: 4px;
padding-left: 4px;
}

.anndata-repr .adata-search-toggle {
display: none; /* Hidden until JS enables */
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
padding: 0;
border: none;
border-radius: 3px;
background: transparent;
color: var(--anndata-text-muted);
font-size: 10px;
font-family: var(--anndata-font-mono);
font-weight: 600;
cursor: pointer;
transition: all 0.15s;
}

.anndata-repr .adata-search-toggle:hover {
background: var(--anndata-bg-secondary);
color: var(--anndata-text-primary);
}

.anndata-repr .adata-search-toggle.active {
background: var(--anndata-accent-color);
color: white;
}

/* Regex error indicator */
.anndata-repr .adata-search-box.regex-error {
border-color: #dc3545;
}

.anndata-repr .adata-search-box.regex-error .adata-search-input {
background: rgba(220, 53, 69, 0.05);
}

/* Metadata bar */
.anndata-repr .adata-metadata {
display: flex;
Expand Down
Loading