Skip to content

mapi_plots

This package has a sanitizer and templates that use only JSON. These are in the core install. The package also has recipes and converters. These are in the recipes extra.

Sanitizer

The JSON-only PlotSpec sanitizer (plan §10.3, step 1).

This is the control-plane half of plot safety. It depends only on mapi-spec and jsonschema and never imports numpy, pandas, altair or plotly (enforced by import-linter and a subprocess test), so mapi_server can run it on every plot it stores or serves.

from mapi_plots.sanitize import sanitize_plot result = sanitize_plot(plot_json) # PlotSpec | dict | str | bytes result.plot # the sanitized PlotSpec result.changes # what was rewritten (HTML stripped, ...)

Anything unsafe raises :class:SanitizeError (a MapiError) whose issues carry the JSON pointer, the :class:SanitizeRule (in context["rule"]) and any user-supplied text in untrusted_text. :func:check_plot returns the same issues without raising (for editors).

SanitizeResult

SanitizeResult(
    plot: PlotSpec, changes: list[Issue] = list()
)

A sanitized plot plus the list of changes the sanitizer made (severity info).

SanitizeError

SanitizeError(
    issues: list[Issue], detail: str | None = None
)

Bases: MapiError

A plot document was rejected. issues lists every violation (capped).

rules

rules: set[SanitizeRule]

The rules that fired.

SanitizeLimits

SanitizeLimits(
    max_document_bytes: int = 16 * 1024 * 1024,
    max_spec_bytes: int = 2 * 1024 * 1024,
    max_depth: int = 48,
    max_nodes: int = 200000,
    max_string_bytes: int = 64 * 1024,
    max_array_items: int = 100000,
    max_expr_chars: int = 2000,
    max_typed_array_bytes: int = 8 * 1024 * 1024,
    max_issues: int = 50,
)

Caps applied before anything else looks at a plot document.

SanitizeRule

Bases: StrEnum

Why the sanitizer rejected (or changed) part of a plot spec. Values are stable.

sanitize_plot

sanitize_plot(
    plot: PlotInput,
    *,
    limits: SanitizeLimits = DEFAULT_LIMITS,
    validate_schema: bool = True,
) -> SanitizeResult

Validate and sanitize a plot document; raise :class:SanitizeError if it is unsafe.

check_plot

check_plot(
    plot: PlotInput,
    *,
    limits: SanitizeLimits = DEFAULT_LIMITS,
    validate_schema: bool = True,
) -> list[Issue]

Every error and change the sanitizer would report, without raising.

sanitize_spec

sanitize_spec(
    kind: str,
    spec: Mapping[str, Any],
    *,
    limits: SanitizeLimits = DEFAULT_LIMITS,
    validate_schema: bool = True,
) -> tuple[dict[str, Any], list[Issue]]

Sanitize a bare Vega-Lite or Plotly spec (no envelope); returns (spec, changes).

Templates

Role/dim-bound plot templates (plan §10.1).

A template is an ordinary :class:~mapi_spec.plots.PlotSpec whose bindings say what to plot in schema terms ({x: "dim:time", y: "role:target", facet: "dim:geo"}) instead of naming one bundle's variables. :func:apply_template resolves the bindings against a :class:~mapi_spec.schema.DataSchema and fills the placeholders in the spec, so one user-authored "fan chart of the target over time, faceted by geo" applies to every model of a family. This module is JSON-only (it runs in the control plane).

Placeholders inside spec:

  • {"$bind": "y"} - replaced by a Vega-Lite field definition {"field": ..., "type": ..., "title": ...}; dropped entirely when the channel is optional and unresolved (so encoding.facet simply disappears for a model without geo);
  • "{{y}}" - the bound name; "{{y.field}}" - the name escaped for Vega-Lite field access (dots); "{{y.type}}" - the Vega-Lite type; "{{y.title}}" - name plus units. Placeholders may be embedded in longer strings (titles, captions).

BoundField

BoundField(
    channel: str,
    expr: str,
    kind: Literal["dim", "var", "coord", "stat"],
    name: str,
    vl_type: VlType,
    units: str | None = None,
)

One binding resolved to a concrete dataset column.

field

field: str

The name escaped for Vega-Lite field access (. is nested access otherwise).

TemplateError

Bases: MapiError

A template's bindings do not resolve against a schema.

apply_template

apply_template(
    plot: PlotSpec,
    schema: DataSchema,
    *,
    optional: Iterable[str] = (),
    title: str | None = None,
) -> PlotSpec

Resolve plot.bindings against schema and fill the spec's placeholders.

resolve_bindings

resolve_bindings(
    bindings: PlotBindings,
    schema: DataSchema,
    *,
    optional: Iterable[str] = (),
) -> dict[str, BoundField | list[BoundField]]

Resolve every binding; tooltips resolve to lists. Optional channels may be missing.

template_issues

template_issues(
    plot: PlotSpec,
    schema: DataSchema,
    *,
    optional: Iterable[str] = (),
) -> list[Issue]

Why a template does not apply to schema (empty list when it does).

Converters

Build :class:~mapi_spec.plots.PlotSpec envelopes from Altair charts and Plotly figures.

Extra recipes only (needs altair, plotly, numpy, pandas and pyarrow).

  • :func:from_altair moves every inline dataset (Altair's consolidated datasets and any data.values) into named Arrow datasets and rewrites the references to {"name": ...}; named data (alt.Data(name=...)) is filled from datasets=.
  • :func:from_plotly decodes plotly.py 6 typed arrays ({dtype, bdata, shape}) and inline data arrays into named Arrow datasets, one per trace (and array length), and binds them with {"$ref": "<dataset>.<column>"}. String columns get the inline-text rules here (HTML stripped to the Plotly allow-list, script/CSS URLs rejected) because the control-plane sanitizer never decodes Arrow; the plot iframe must still re-strip dataset strings.

Both run the result through the sanitizer unless sanitize=False. Datasets are base64 Arrow IPC streams (:class:~mapi_spec.plots.InlineArrow); datetimes are stored as UTC millisecond timestamps, which JavaScript reads as numbers.

from_altair

from_altair(
    chart: Any,
    *,
    id: str,
    title: str | None = None,
    datasets: Mapping[str, TableLike] | None = None,
    persona: Persona = "both",
    caption_plain: str | None = None,
    bindings: PlotBindings | None = None,
    recipe: str | None = None,
    plugin: str | None = None,
    author: str | None = None,
    bundle_id: str | None = None,
    sanitize: bool = True,
    compact: bool = False,
) -> PlotSpec

Convert an Altair chart (or a Vega-Lite dict) into a sanitized PlotSpec.

from_plotly

from_plotly(
    fig: Any,
    *,
    id: str,
    title: str | None = None,
    persona: Persona = "both",
    caption_plain: str | None = None,
    bindings: PlotBindings | None = None,
    recipe: str | None = None,
    plugin: str | None = None,
    author: str | None = None,
    bundle_id: str | None = None,
    keep_template: bool = False,
    min_length: int = 1,
    sanitize: bool = True,
) -> PlotSpec

Convert a Plotly figure (go.Figure or dict) into a sanitized PlotSpec.

Data arrays (typed or inline) move into Arrow datasets named trace<i> (frame<f>_ trace<i> for animation frames); arrays shorter than min_length stay inline. The default plotly.py template is dropped unless keep_template (the renderer themes).

Recipes

The standard plot recipe catalog (plan §10.2). Extra recipes only.

Every recipe is a function (DataTree-like, settings) -> PlotSpec with its data in named Arrow datasets (never URLs), a persona and a plain-language caption. They are generated in the runner from a fitted bundle or a score output, so every plugin gets them for free:

============================ ========================================================== core.data_overview series per variable, missingness heatmap, group sizes core.prior_predictive prior predictive densities (+ fan over time) core.convergence R-hat / ESS per parameter, worst-parameter table, trace, rank core.posterior_forest point, 50% and 94% HDI per parameter core.ppc predictive density overlay, PIT histogram, coverage core.ppc_fan fit + forecast fan with the split line core.fit_vs_actual predicted against actual core.residuals_over_time actual minus predicted over time core.decomposition stacked contributions and totals core.model_comparison elpd forest (precomputed LOO table, or WAIC) core.counterfactual scenario vs baseline band + total-change distribution core.decision_distribution scalar decision objective per option core.pair corner plot of a few parameters, divergences highlighted core.ridge one density per level of a dim, ordered by mean core.rank_matrix P(rank = k) per item and pairwise P(i above j) core.calibration PIT-ECDF difference with an envelope, or a reliability diagram core.state_sequence observed series above stacked state probabilities core.roc_pr ROC and precision-recall curves of a binary classifier core.confusion 2 x 2 confusion matrix at a probability threshold core.loss_curve loss of an iterative fit (ADVI) per step, moving average core.loo_pit LOO-PIT calibration and the PSIS Pareto k per observation core.energy marginal energy vs energy transitions, E-BFMI per chain core.prior_posterior prior and posterior densities per parameter, ranked by shift ============================ ==========================================================

The MMM domain pack lives in :mod:mapi_plots.packs.mmm and the tabular-ML pack (ml.feature_importance) in :mod:mapi_plots.packs.ml, and the scientific-model pack (sci.field_map, sci.vector_field, sci.profile_bands, sci.ensemble; Plotly) in :mod:mapi_plots.packs.sci, with the map, tree and polar recipes in :mod:mapi_plots.packs.sci_maps, the image and spectrum recipes in :mod:mapi_plots.packs.sci_images and sci.point_pattern in :mod:mapi_plots.packs.sci_points, the network pack (net.graph; Plotly) in :mod:mapi_plots.packs.net, and the causal pack (causal.impact, causal.pre_fit, causal.donor_weights, causal.event_study) in :mod:mapi_plots.packs.causal. All are exposed through the mapi.plots entry-point group, so mapi_sdk.registry.default_registry() finds them.

Recipe

Recipe(
    id: str,
    title: str,
    settings: type[S],
    fn: Callable[[RecipeInput, S], Figure],
    version: str = "1.0.0",
    persona: Persona = "both",
    requires_groups: tuple[str, ...] = (),
    requires_representation: tuple[
        Representation, ...
    ] = (),
    summary: str = "",
    tags: tuple[str, ...] = tuple(),
    about: About | None = None,
    example: ExampleFn | None = None,
)

A registered plot recipe.

example_spec

example_spec() -> PlotSpec | None

The recipe rendered on its synthetic example data (None when it has none): example if given, else the bundled one (:mod:mapi_plots.examples).

render

render(data: Any, **options: Any) -> PlotSpec

Render the recipe for data (DataTree, Dataset or mapping of groups).

RecipeSettings

Bases: BaseModel

Common recipe options. Subclasses add their own (extra keys are rejected).

Figure

Figure(
    chart: Any,
    datasets: dict[str, DataFrame],
    caption: str,
    title: str | None = None,
    bindings: PlotBindings | None = None,
    kind: Literal["auto", "vega-lite", "plotly"] = "auto",
)

What a recipe function returns.

chart is an Altair chart (or a Vega-Lite dict), or a Plotly figure (go.Figure or a {data, layout, frames} dict). kind tells them apart; auto detects it.

get_recipe

get_recipe(id: str) -> Recipe[Any]

Look a bundled recipe up by id (e.g. "core.ppc_fan").

standard_recipes

standard_recipes() -> list[Recipe[Any]]

The core catalog (entry point mapi.plots: core).

render_plots

render_plots(
    data: Any,
    refs: Iterable[str],
    *,
    registry: Any | None = None,
    options: Mapping[str, Mapping[str, Any]] | None = None,
) -> RenderReport

Render every ref against data; problems become warnings (plugins: errors).

options maps a recipe id to its settings. registry is a mapi_sdk Registry (plugin-provided recipes); without one only the bundled recipes resolve.

recipe

recipe(
    id: str,
    *,
    title: str,
    settings: type[RecipeSettings] = RecipeSettings,
    version: str = "1.0.0",
    persona: Persona = "both",
    requires_groups: Iterable[str] = (),
    requires_representation: Iterable[Representation] = (),
    summary: str = "",
    tags: Iterable[str] = (),
    about: About | None = None,
    example: ExampleFn | None = None,
) -> Callable[
    [Callable[[RecipeInput, Any], Figure]], Recipe[Any]
]

Decorator turning fn(inp, settings) -> Figure into a registered :class:Recipe.

about is the plot's About card (built-in recipes take theirs from :data:mapi_spec.plot_abouts.PLOT_ABOUTS); example returns synthetic (data, options) for the catalog's example render.

render_template

render_template(
    template: PlotSpec,
    data: Any,
    schema: DataSchema | None = None,
    *,
    group: str | None = None,
    value: str | None = None,
    optional: Iterable[str] = (),
    dataset: str | None = None,
    max_rows: int = 50000,
) -> PlotSpec

Fill a template's placeholders and its dataset from data; returns a sanitized plot.

value names the variable whose stat coordinate feeds stat: bindings (default: the single target, else the first variable with a stat dim).

builtin_templates

builtin_templates() -> dict[str, PlotSpec]

Role/dim-bound templates that apply to any model family with matching roles.