Writing an extension
An extension is an ordinary Python distribution that registers one entry point. Do that, build
your charts on the public dysonsphere.ext surface, and they resolve under ds.<name> and
behave like built-ins. This guide covers packaging, entry point registration, the ext
primitives, and tagging generated data.
Package layout
Section titled “Package layout”Ship a normal top-level package with its own dependencies. dysonsphere is a dependency, not
something you vendor:
dysonsphere-yourname/├── pyproject.toml└── dysonsphere_yourname/ ├── __init__.py └── yourchart.pyfrom .yourchart import yourchart
__all__ = ["yourchart"]Cap the core dependency below the next major so a future breaking core release fails the resolver loudly rather than breaking your charts silently:
[project]name = "dysonsphere-yourname"dependencies = [ "dysonsphere>=3.0.0,<4", "altair>=5.5.0", "polars[pyarrow]>=1.19.0", # your domain deps here - lazy-import the heavy/optional ones (see below)]Entry point registration
Section titled “Entry point registration”One table in pyproject.toml registers your package under the
dysonsphere.extensions group, keyed by the name you want on ds:
[project.entry-points."dysonsphere.extensions"]yourname = "dysonsphere_yourname"Now pip install dysonsphere-yourname makes ds.yourname.yourchart(...) work, and
ds.extensions() lists yourname - with no change to core. That’s the entire integration
contract.
The dysonsphere.ext surface
Section titled “The dysonsphere.ext surface”Your chart is just a function that returns an Altair object. To make it a first-class
dysonsphere chart - themed, darkmode-aware, and correctly handled by ds.read() - use the
stable primitive surface, never _-private internals:
import altair as altimport polars as plimport dysonsphere as dsfrom dysonsphere import ext
def yourchart(df, *, xCol="x", yCol="y") -> ext.AltairChart: data = ds.ensure_polars(df) # accept polars or pandas
darkmode = bool(ext.opt("darkmode")) # read the active theme accent = ds.colors["blues2"][10 if darkmode else 2]
points = alt.Chart(data).mark_point().encode( x=alt.X(f"{xCol}:Q"), y=alt.Y(f"{yCol}:Q"), )
# A generated helper layer (e.g. a reference band) - TAG it (see step 4). band = alt.Chart(ext.internal_data([{"lo": 0, "hi": 1}])).mark_rect( color=accent, opacity=0.1, ).encode(y="lo:Q", y2="hi:Q")
return band + pointsThe ext surface is deliberately minimal - promoted primitive by primitive as real extensions
need them. Today it is exactly three names:
ext.AltairChart- the chart-object union (Chart | LayerChart | FacetChart | VConcatChart | HConcatChart | ConcatChart). Use it as your return annotation, matching core’s ownsave().ext.opt(key)- read an active-theme option (opt("markSize"),opt("chartWidth"),opt("darkmode"), …). It falls back to the derived built-in default when called before anyds.theme(), so your styling code never seesNonesentinels. This is the only supported way to read theme options outside core.ext.internal_data(data)- tag a generated sidecar dataset (next section).
Because colors resolve from the theme at call time, a chart exported for both light and dark
must be built inside a ds.save() callable so it re-runs per variant:
ds.save(lambda: ds.yourname.yourchart(df), "figure")Tagging generated data
Section titled “Tagging generated data”Composite charts often generate their own small “sidecar” datasets - reference bands, computed
label coordinates, fit lines. Altair inlines each as a separate dataset, so ds.read(what="data")
needs to tell your generated data from the user’s frame.
The rule: route every dataset you computed through ext.internal_data(...), and never tag
the frame the caller handed you.
# generated data -> tag it, so read(what="data") filters it outalt.Chart(ext.internal_data(computed_rows))
# the user's own frame -> DO NOT tag it (tagging hides their data from read())alt.Chart(ds.ensure_polars(df))internal_data accepts a list[dict] (→ alt.Data) or a polars/pandas DataFrame (→ a tagged
polars frame). Miss a sidecar and it leaks as a phantom “user” dataframe - a false multi-frame
error from read, or the wrong frame returned. Tag the user’s frame and you hide their real
data. So: data you computed → tag it; data the caller passed in → leave it.
Lazy imports for heavy dependencies
Section titled “Lazy imports for heavy dependencies”Domain packages often need heavyweight, optional libraries (genome-file parsers, image stacks).
Import them inside the function, not at module top level, with a clear error - so
import dysonsphere_yourname stays cheap and only the charts that need the dependency require it:
def yourchart(df, ...): try: import pyBigWig except ImportError as e: raise ImportError("yourchart() needs pyBigWig: pip install pyBigWig") from e ...Reference implementation
Section titled “Reference implementation”dysonsphere-biology is the worked example - its volcano() is built
entirely on core + ext, tags its label/threshold layers through internal_data, resolves
colors from the theme, and lives in-repo as a uv workspace member. Read its source alongside this
guide.