Skip to content
dysonsphere

Saving, reading, loading

ds.save() is more than a renderer: it embeds provenance, the resolved theme, any statistics, and your data into the exported files, so every figure is its own lab-notebook entry. ds.read() and ds.load() pull that information back.

import dysonsphere as ds
ds.theme()
# ... build `chart` ...
ds.save(chart, "figure")

By default this writes figure.svg and figure.json. Control the outputs with format= (any of "svg", "png", "json", "html" - a string or a list) and background= ("light", "dark", or both). A two-background export gets _light / _dark suffixes; a single background gets clean names.

ds.save(chart, "figure", format=["svg", "png", "json"], background=["light", "dark"])
ds.save(chart, "figure", format="html") # a self-contained interactive HTML page

The SVG post-processing means a notebook’s inline preview (Altair renders straight from Vega-Lite) can differ subtly from the saved file. ds.show(chart) renders a chart through the save() pipeline so the preview matches the exported figure exactly.

The SVG is post-processed for publication: <g> wrappers are flattened for Illustrator, axis ticks and log/power minor ticks are snapped to exact positions, and superscript exponents are realigned.

When saveMetadata=True (the default), save() writes a dysonsphere metadata block into all three formats:

  • provenance - who/when/what plus the full rendering environment: the OS, the versions of Python, Altair, vl-convert (the renderer), dysonsphere (and any extensions that produced the figure), numpy, scipy, and polars - then the machine-identity checksums.
  • theme - the resolved ds.theme() arguments, so the look can be reproduced exactly.
  • statistics - the structured records from any add_comparisons() / add_correlation() on the chart (exact p-values, effect sizes, descriptives).
  • report - the human-readable version of those statistics.

This is not hand-waving - here is the actual block from a real save. Take a strip plot with a Kruskal-Wallis comparison on the classic cars dataset:

chart = ds.mark_strip(cars, "Origin", "Miles_per_Gallon", origins) + ds.add_comparisons(
cars, "Origin", "Miles_per_Gallon",
[("USA", "Europe"), ("USA", "Japan")],
test="kruskal", omnibusVerbose=True, categories=origins,
)
ds.save(chart, "mpg_by_origin", format="json")

ds.read("mpg_by_origin.json", what="metadata") returns a dict with four keys - provenance, statistics, theme, and report. The provenance leads with who/when/what, then the full rendering environment (the OS and vl-convert pin how text was measured, beyond the library versions), and ends with the machine-identity checksums:

{
"user": "dkkung",
"script": "mpg_by_origin.py",
"timestamp": "2026-07-06T23:47:54Z",
"environment": {
"os": "macOS-26.5.2-arm64-arm-64bit-Mach-O",
"python": "3.13.12", "altair": "6.2.1", "vl_convert": "1.9.0.post1",
"dysonsphere": "3.1.0", "numpy": "2.4.6", "scipy": "1.17.1", "polars": "1.41.2"
},
"vegaliteChecksum": "sha256:1ea9d534f84a33c6b550d805d9ad967f0e36f415b2aa4b491dc2402cfd57e841",
"exportIdentifier": "af751cd3-43cb-4076-8751-02508e54ae38",
"dataChecksum": ["sha256:281a94603a8549cda02866679f34c8d0d5ed490d3944c5ab881423ff388b52e8"]
}

The three checksums answer different questions: dataChecksum (one per dataframe, order-independent) is the data identity - two charts of the same numbers match even if drawn differently; vegaliteChecksum is the exact spec content; exportIdentifier is shared by every file from one save() call. A figure produced by an extension (say ds.biology.volcano) additionally records "dysonsphere-extensions": {"biology": "0.1.0"} in environment - only extensions that actually built the chart, not merely installed ones.

The statistics entry is a list of structured records - exact p-values and effect sizes, not the rounded on-plot labels. One omnibus record from the chart above (trimmed):

{
"kind": "omnibus",
"dataChecksum": "sha256:d99fd99e862f635c276ed548bd0fb28602ba50e51bf7437b3847ea83c241e01c",
"test": "kruskal",
"groups": [
{"label": "Europe", "n": 70, "mean": 27.89, "sd": 6.72, "median": 26.5, "q1": 24.0, "q3": 30.65},
{"label": "Japan", "n": 79, "mean": 30.45, "sd": 6.09, "median": 31.6, "q1": 25.7, "q3": 34.05},
{"label": "USA", "n": 249, "mean": 20.08, "sd": 6.40, "median": 18.5, "q1": 15.0, "q3": 24.0}
],
"omnibus": {
"name": "Kruskal-Wallis",
"statistic": {"symbol": "H", "value": 134.46, "df": [2]},
"pvalue": 6.354e-30,
"effect": {"name": "epsilon_squared", "symbol": "ε²", "value": 0.339}
},
"comparisons": {
"test": "dunn", "correction": null,
"pairs": [
{"group1": "Europe", "group2": "USA", "pvalue": 5.32e-14, "effect": {"symbol": "r", "value": -0.615}},
{"group1": "Japan", "group2": "USA", "pvalue": 5.93e-25, "effect": {"symbol": "r", "value": -0.746}}
]
}
}

And the report key is that same content rendered as the plain-text lab note you get from ds.read("mpg_by_origin.json") (the default what="report"):

Statistics | Omnibus | Kruskal-Wallis
─────────────────────────────────────
H(2) = 134, P = 6.35e-30
Effect size: ε² = 0.339
Group descriptives:
Europe n=70 mean=27.9 sd=6.72 median=26.5 IQR=[24, 30.6] range=[16.2, 44.3]
Japan n=79 mean=30.5 sd=6.09 median=31.6 IQR=[25.7, 34] range=[18, 46.6]
USA n=249 mean=20.1 sd=6.4 median=18.5 IQR=[15, 24] range=[9, 39]
Post-hoc (dunn):
Europe vs Japan P = 0.0555 r = 0.267
Europe vs USA P = 5.32e-14 r = -0.615
Japan vs USA P = 5.93e-25 r = -0.746
Provenance
──────────
Generated by dkkung with mpg_by_origin.py on 2026-07-06T23:47:54Z using
macOS-26.5.2-arm64-arm-64bit-Mach-O, Python 3.13.12, Altair 6.2.1, vl-convert 1.9.0.post1,
dysonsphere 3.1.0, NumPy 2.4.6, SciPy 1.17.1, Polars 1.41.2.
Data checksum: sha256:281a9460…
Vega-Lite spec checksum: sha256:1ea9d534…
Export identifier: af751cd3-43cb-4076-8751-02508e54ae38

The data itself round-trips too. ds.read("mpg_by_origin.json", what="data") returns the original 398-row dataframe - every column Altair inlined, including transform columns like jitter_x (the honest data of record), with dysonsphere’s own sidecar layers (bracket coordinates, error bars) filtered out.

ds.read("figure.json") # the human-readable report (default)
ds.read("figure.json", what="statistics") # structured records (list of dicts)
ds.read("figure.json", what="metadata") # the whole dysonsphere block (shown above)
ds.read("figure.json", what="data") # the original DataFrame

read() works on JSON, SVG, and PNG for metadata and reports; what="data" is JSON-only (only the JSON carries the full data). what="data" returns the user’s frame, filtering out dysonsphere’s internal sidecar datasets; pick the output type with output= ("polars", "pandas", "duckdb", or "records").

chart = ds.load("figure.json") # an Altair object, theme re-applied
spec = ds.load("figure.json", raw=True) # the untouched Vega-Lite spec dict

load() is JSON-only (it needs the full spec) and re-applies the baked theme so the reconstructed chart renders identically.

add_comparisons() and add_correlation() can surface their report directly:

chart = base + ds.add_comparisons(df, "group", "value", pairs, test="anova", report=True)
# report=True prints it; save="dir/" writes a timestamped .txt

See the Saving & loading and Reading exports reference pages for the full API.