Source code for phenotypic.abc_.plotting._output

"""Backend-neutral single and multi-page plot output values."""

from __future__ import annotations

import json
import math
from dataclasses import dataclass, field
from datetime import date, datetime, timedelta
from typing import Any, Mapping, TypeAlias

FigureLike: TypeAlias = Any


[docs] @dataclass(frozen=True) class PlotPage: """One independently saveable figure page. Args: key: Stable logical page key. figure: Plotly or Matplotlib figure. label: Optional human-readable page label. metadata: Immutable-by-convention selector metadata. """ key: str figure: FigureLike label: str | None = None metadata: Mapping[str, str | int | float | bool | None] = field( default_factory=dict ) def __post_init__(self) -> None: if not isinstance(self.key, str) or not self.key: raise ValueError("plot page key must be a non-empty string")
[docs] @dataclass(frozen=True) class PlotOutput: """Ordered pages returned by a plotting invocation.""" pages: tuple[PlotPage, ...] def __post_init__(self) -> None: keys = [page.key for page in self.pages] duplicates = sorted({key for key in keys if keys.count(key) > 1}) if duplicates: raise ValueError(f"plot output contains duplicate page keys: {duplicates}")
[docs] def canonical_group_key( pairs: list[tuple[str, Any]], ) -> str: """Encode typed grouping pairs without ambiguous string concatenation. Integer, float, temporal, and string values retain distinct tags. Numeric and temporal payloads use canonical strings so JSON encoder settings and platform formatting cannot change a page identity. """ encoded: list[list[str | None]] = [] for column, value in pairs: if hasattr(value, "item"): value = value.item() if value is None: kind = "null" canonical = None elif isinstance(value, bool): kind = "bool" canonical = "true" if value else "false" elif isinstance(value, datetime): kind = "datetime" canonical = value.isoformat() elif isinstance(value, date): kind = "date" canonical = value.isoformat() elif isinstance(value, timedelta): kind = "timedelta_ns" canonical = str( ( value.days * 86_400 + value.seconds ) * 1_000_000_000 + value.microseconds * 1_000 ) elif isinstance(value, int): kind = "int" canonical = str(value) elif isinstance(value, float): if not math.isfinite(value): raise ValueError("grouping floats must be finite") kind = "float" canonical = value.hex() elif isinstance(value, str): kind = "str" canonical = value else: raise TypeError( f"unsupported grouping value {value!r} " f"({type(value).__name__})" ) encoded.append([column, kind, canonical]) return json.dumps(encoded, ensure_ascii=False, separators=(",", ":"))
__all__ = [ "FigureLike", "PlotOutput", "PlotPage", "canonical_group_key", ]