Adding plots¶
How to add a new plot (or plot family) to cellpy's shared plotting stack.
User-facing drawing helpers (summary_plot, cycles_plot, raw_plot,
cycle_info_plot, ica_plot, dva_plot) still live in
cellpy.utils.plotutils. The machinery they call lives in
cellpy.plotting. Do not add new layout logic in plotutils or revive
the old plotutils / collectors forks — extend the prepare → spec → render
path instead.
API reference: Plotting.
Pipeline¶
Every public plot entry follows the same shape:
cell / frame
→ context (CellContext / FrameContext)
→ registry (PlotFamily)
→ prepare → (tidy DataFrame, FigureSpec)
→ get_backend(name).render(frame, spec)
summary_plot is the reference orchestration (simplified):
from cellpy.plotting.backends import get_backend
from cellpy.plotting.context import from_source
from cellpy.plotting.prepare.summary import prepare as prepare_summary
import cellpy.plotting.registry as plot_registry
family = plot_registry.get(config.y)
ctx = from_source(c)
frame, spec = prepare_summary(ctx, family, config, plot_info=plot_info)
fig = get_backend(resolved_backend).render(frame, spec)
| Piece | Package | Role |
|---|---|---|
| Context | cellpy.plotting.context |
Thin adapter over a CellpyCell or a collected frame |
| Registry | cellpy.plotting.registry |
Named PlotFamily records (y= menus and non-summary kinds) |
| Spec | cellpy.plotting.spec |
Frozen FigureSpec / PanelSpec / AxisSpec |
| Prepare | cellpy.plotting.prepare.* |
Build tidy frame + FigureSpec |
| Backends | cellpy.plotting.backends |
plotly and matplotlib renderers |
Multi-cell figures use cellpy.plotting.collected (collected_plot) and
batch cycle-life plots use cellpy.plotting.batch_summary — same backend
render contract, different prepare/entry points.
Where code lives¶
cellpy/plotting/
├── registry.py # PlotFamily + _register_family
├── spec.py # FigureSpec / PanelSpec / AxisSpec
├── context.py # CellContext / FrameContext
├── prepare/
│ ├── summary.py # summary_plot families
│ ├── curves.py # cycles_plot
│ ├── raw.py # raw_plot
│ ├── steps.py # cycle_info_plot
│ └── ica.py # ica_plot / dva_plot
├── backends/
│ ├── base.py # Backend protocol
│ ├── plotly.py
│ └── mpl.py
├── collected.py
└── batch_summary.py
Public entry points in cellpy.utils.plotutils should stay thin: parse args,
build a small config object, then run context → registry → prepare → render.
Choose the right extension point¶
Pick the smallest change that fits:
- New
summary_plot(y=...)family — same summary tidy frame and summary render path; only register a newPlotFamily(and update the oracle menu expectations if the built-in list changes). - New public plot function (like
cycles_plot) — new family withextras["entry_point"]/extras["kind"], a prepare module that setsspec.extras["kind"], backend branches for that kind, and a thinplotutilswrapper. - Collected / batch — extend
collected_plotorbatch_summary_plotrather than inventing a third layout path.
Unknown summary_plot y= values fail loudly (ValueError listing known
names). There is no raw-column fallthrough.
Add a summary family¶
Summary families are the y= names summary_plot accepts. They are registered
in cellpy/plotting/registry.py via _register_family (called from
_register_builtin_families at import time).
Minimal shape:
from cellpy.plotting.registry import PlotFamily, _register_family
_register_family(
PlotFamily(
name="my_family",
description="Short menu text for apps / docs",
column_builder=lambda hdr: [
hdr.charge_capacity + "_gravimetric",
hdr.discharge_capacity + "_gravimetric",
],
mode="gravimetric", # optional unit/mode hint used by prepare
# supports_formation=True,
# supports_cv_split=False,
# transforms_builder=..., # optional derived/normalized columns
)
)
Notes:
- Prefer header-bound column names through
column_builder(hdr)(c.schema.summary/get_headers_summary()), not hard-coded 1.x strings. mod_01_<column>marks a derived series (seePlotFamily.summary_optionsand existing full-cell families)._register_familyis provisional for in-tree use in 2.0; apps should not rely on it as a stable public plugin API yet.- Default
entry_pointis"summary_plot". Omitextrasfor summary families so they stay on the summary oracle menu.
After adding a built-in summary family, update
EXPECTED_SUMMARY_FAMILIES in tests/test_plotting_registry.py so the
registry/oracle contract stays explicit. The runtime oracle menu in
tests/figure_spec_support.py (SUMMARY_FAMILIES) is derived from
families(entry_point="summary_plot") and must not be hand-edited.
Add a non-summary plot (new kind)¶
Families that are not summary_plot(y=...) names register with scoped
extras, for example:
PlotFamily(
name="cycles",
description="Voltage vs capacity by cycle",
column_builder=lambda hdr: ["capacity", "potential", "cycle_num"],
extras={"entry_point": "cycles_plot", "kind": "cycles"},
)
Then:
- Prepare — add
cellpy/plotting/prepare/<name>.pywithprepare(ctx, ...) -> (frame, FigureSpec). Setspec.extras["kind"]to the same kind string backends will switch on. Export it fromcellpy/plotting/prepare/__init__.pywhen it is part of the public prepare surface. - Backends — teach both
PlotlyBackendandMatplotlibBackendto handle thatkind(see the existingcycles/raw/cycle_info/ica/dvabranches). Keep layout decisions in the backends (or in prepare-producedFigureSpec/extras), not inplotutils. - Public wrapper — add or extend a function in
cellpy.utils.plotutilsthat mirrors thesummary_plotorchestration above. - Registry filter — keep non-summary families out of the summary menu by
setting
extras["entry_point"]to the owning public function name.
Labels, units, and themes¶
Reuse shared helpers instead of hard-coding axis text:
cellpy.plotting.labels—quantity_label/units_quantity_labelcellpy.plotting.theme— plotly templates and colour cyclescellpy.plotting.cycle_legend— legend vs colorbar policy for cycle colouringcellpy.plotting.figures— load/save /write_image(kaleido / batch extra)
Prefer schema-resolved header names and unit helpers from cellpy.units
over literal column strings.
Tests and the figure oracle¶
Minimum expectations when you add a plot path:
| Layer | What to cover |
|---|---|
| Registry | Family name, description, entry_point, resolved columns |
| Prepare | Returns a tidy frame + FigureSpec with the right kind / panels |
| Backend | Both plotly and matplotlib render without raising (mark deps as needed) |
| Public API | Thin wrapper accepts the documented kwargs and returns a figure |
Useful anchors:
tests/test_plotting_registry.py— built-in menu +_register_familyround-triptests/figure_spec_support.py— figure oracle menu (SUMMARY_FAMILIES,FigureCasebuilders)- Existing prepare/backend tests under
tests/for the plot family you are mirroring (search for thekindor public function name)
Run a focused slice while iterating, for example:
Plotting tests that draw figures should run under MPLBACKEND=Agg (CI already
sets this where needed). Prefer extending the oracle/menu helpers over
one-off snapshot tests when the change is “another family on the same path”.
Checklist¶
- Chose summary family vs new
kind/ entry point - Registered
PlotFamily(header-bound columns; correctextras) - Prepare returns
(frame, FigureSpec)and setsspec.extras["kind"]when needed - Both backends handle the path (or clearly share the summary branch)
- Public
plotutils(or collected/batch) wrapper stays orchestration-only - Registry/oracle expectations updated for new built-in summary names
- Tests cover registry + prepare/render smoke for the new path
- User docs / API notes updated if the public surface changed