Skip to content

release 0.5.0: strain_label_styles, selection-param charts via JSON/CLI - #13

Merged
jbloom merged 4 commits into
mainfrom
strain-label-styles
Oct 30, 2026
Merged

jbloom merged 4 commits into
mainfrom
strain-label-styles

Conversation

@jbloom

@jbloom jbloom commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Closes #12. Bumps the version to 0.5.0. This PR has four commits:

  • the bug fix;
  • the feature;
  • the release bump;
  • a CLAUDE.md update to the release step.

If this is squash-merged, this description is what keeps the fix's rationale in main's history.

strain_label_styles: style a subset of the tree-panel labels (#12)

A list of rules. Each rule names some strains and a style built from Vega-Lite text properties: fill, opacity, fontWeight and fontStyle. An optional when turns a rule on only while it is truthy. It can be:

  • the name of a param declared in the chart, e.g. a checkbox;
  • any Vega expression, e.g. "emphasis == 'cell'" for a radio button;
  • in Python, an alt.Parameter, which is declared on the returned chart if the chart doesn't already declare it.
highlight = alt.param(name="vaccine_strains", value=True, bind=alt.binding_checkbox(name="vaccine strains: "))
plot(
    ...,
    connect_leader_to_label=True,
    strain_label_styles=[
        {"strains": ["K", "J.2.4", "J.2:S145N"], "style": {"fill": "#d62728", "fontWeight": "bold"}, "when": highlight},
    ],
)

On the CLI the rules are a JSON array, and when names a param saved in the chart JSON:

--connect-leader-to-label --strain-label-styles '[{"strains": ["K"], "style": {"fill": "#d62728", "fontWeight": "bold"}, "when": "vaccine_strains"}]'

No layer duplication. The issue assumed a mark-property ExprRef can't see datum. In fact, Vega-Lite compiles a mark-level {"expr": ...} into the Vega encode block, where datum is available. So each styled property becomes one per-label expression on the existing halo and text layers: a ternary chain over per-rule membership columns in the label data.

  • The halo follows the glyph-shape properties (fontWeight, fontStyle), so it keeps masking the leader lines.
  • The returned chart keeps its shape, which also covers a scale bar and any future merge of the halo and text marks.

Semantics:

  • Later rules win, property by property.
  • Strains must be tips of the input tree; strains dropped by prune_tree_to_chart are skipped.
  • Unknown style keys raise, with hints for color → fill and for fontSize (excluded because it changes the label-strip width estimate).
  • A bare when name must be a declared variable param; selection params are rejected.
  • The parameter requires connect_leader_to_label=True. With it off, the labels are the chart's own axis labels, and the error points to Vega-Lite's conditional axis-label properties.
  • Params are appended to the combined chart's top-level params. add_params recurses into sub-charts and raises on charts loaded from a spec.

Fix: charts with selection params failed via JSON / HTML / dict (every CLI call)

A compound chart (layer, facet, concat) with a selection param failed to render when given as a JSON path, HTML path or dict, with Duplicate signal name: "<selection>_tuple". This included the flu examples' CLI reproductions.

  • Cause: _load_chart stripped views from every param. altair keeps a compound sub-chart's existing views when nesting, so the params came out as views: [], and Vega-Lite defines such a selection in every view.
  • Not affected: live altair charts never went through the strip.

Why the strip was there, and why removing it is safe:

  • Phase 2b (0812a7b) found that nesting a chart's dict inside a dict-level concat fails validation with 'views' was unexpected, because views is only valid on top-level selection params. That phase avoided dict round-trips entirely.
  • Phase 2c (ded7beb) reused the note for the new loader, but applied it before a top-level from_dict, where views is valid.
  • I reproduced both with the chart JSON from that day and the altair installed then (6.1.0).
  • No test rendered JSON-path output. The phase 2c and current suites both pass unchanged with the strip disabled.
  • On altair 6.0.0, 6.1.0, 6.2.2 and 6.3.0, the strip leaves only unit charts rendering (2 of 12 cases), and removing it fixes all 12. So the altair>=6.0,<7 floor stays.

Also

  • The plot() Returns docstring now gives the panel order for each tree_location. For a strain on x, the default "bottom" puts the chart first, not the tree.
  • New docs example: H3N2 vaccine strains in bold red behind a checkbox.
  • vaccine_axis_labels helper in the flu example module.
  • CLAUDE.md notes: loaded specs go to from_dict unmodified; tests may assert discrete SVG attributes; a release no longer leaves an empty ## [Unreleased] section above the new version.

Testing

  • scripts/check.sh passes: 293 tests, ruff, black. That includes 52 new label-style tests and a views regression test that fails in all 10 cases on the old loader.
  • The full suite also passes on altair 6.0.0 and 6.3.0.
  • scripts/build_docs.sh builds cleanly in strict mode.
  • flu-seqneut-2026: I ran its own plot_titer_summaries.py (commit 0b2b936) under altair 6.0.0, 6.2.2 and 6.3.0.
    • All 10 tree charts style exactly the vaccine labels, whether plot() declares the checkbox param or their base_chart does.
    • The param is declared once, even after their finalize() re-nests the chart.
    • Their unstyled output is identical to 0.4.0.
  • CLI with the flu H3N2 chart: writes SVG and HTML with the labels styled. The old code fails on the same chart.
  • Not yet done: an eyeball pass over site/examples.html, including toggling the new checkbox.

🤖 Generated with Claude Code

jbloom and others added 4 commits October 30, 2026 08:17
A chart with a selection param on a compound chart (layer, facet, concat)
failed to render when passed to `plot()` as a JSON path, HTML path or dict,
i.e. on every CLI call: Vega-Lite reported `Duplicate signal name:
"<selection>_tuple"`. `_load_chart` deleted `views` from every param before
`alt.Chart.from_dict`. When `_build` then nests the chart, altair keeps a
compound sub-chart's existing `views`, so the stripped params came out as
`views: []`, and Vega-Lite defines such a selection in every view. Live
charts never went through the strip, which is why the docs assets and
downstream Python users (e.g. flu-seqneut-2026) never hit it.

Why the strip existed: phase 2b (0812a7b) found that nesting a chart's
*dict* inside a dict-level concat fails validation with "'views' was
unexpected". `views` is only valid on top-level selection params, and nested
params are validated against the nested schema. Phase 2b avoided dict
round-trips altogether. Phase 2c (ded7beb) reused that note verbatim for the
new loader and stripped `views` before a *top-level* `from_dict`, where they
are valid. Reproducing it with the period chart JSON and the altair installed
then (6.1.0):
- top-level `from_dict` accepts `views`;
- only the dict-nesting approach rejects them;
- stripping doesn't rescue that approach either ("'select' is a required
  property").
The docstring's "the renderer rebinds params to views on its own" is not
true for an emptied list. No test rendered JSON-path output, and the phase 2c
and current suites both pass unchanged with the strip disabled.

Removing it is safe on every supported altair. On 6.0.0, 6.1.0, 6.2.2 and
6.3.0:
- with the strip, only unit charts (2 of 12 shape x tree-side cases) render;
- without it, all 12 do.
So the `altair>=6.0,<7` floor stays.

The `deepcopy` stays: `from_dict` aliases the input's `datasets` row lists.
The new regression test passes dict and JSON-path charts of each compound
shape, and checks both that every selection keeps `views` naming real views
and that vl-convert renders the result. It fails on the old loader in all 10
cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`strain_label_styles` is a list of rules. Each names some `strains` and a
`style` of Vega-Lite text properties (`fill`, `opacity`, `fontWeight`,
`fontStyle`). An optional `when` condition can be:
- a param name declared in the chart, e.g. a checkbox;
- any Vega expression, e.g. "emphasis == 'cell'" for a radio button;
- in Python, an `alt.Parameter`, which is declared on the returned chart if
  the chart doesn't declare it.

Where rules overlap, later rules win property by property. The rules require
`connect_leader_to_label=True`; with it off the labels are the chart's own
axis labels, and the error says so. On the CLI it's a JSON array, and `when`
names a param saved in the chart JSON.

No layers are duplicated. #12 assumed a mark-property ExprRef can't see
`datum`, but Vega-Lite compiles a mark-level `{"expr": ...}` into the Vega
encode block, where it can. So each styled property becomes one per-label
expression on the existing halo and text layers: a ternary chain over
per-rule membership columns in the label data. The halo follows the
glyph-shape properties (fontWeight / fontStyle), so it keeps masking the
leaders; `fill` and `opacity` apply to the text only. The returned chart
keeps its shape, which also covers a scale bar and any future merge of the
halo and text marks. Params are appended to the combined chart's top-level
`params`: `add_params` recurses into sub-charts and raises on spec-loaded
ones.

Validation fails fast:
- strains must be tips of the input tree, and the error shows samples from
  both sides; strains dropped by `prune_tree_to_chart` are skipped;
- unknown style keys raise, with hints for `color` and `fontSize` (the latter
  is excluded because it would change the label-strip width estimate);
- a bare `when` name must be a declared variable param; selection params are
  rejected.

Checked against flu-seqneut-2026's own `plot_titer_summaries.py` (it nests
our output in a further vconcat) under altair 6.0.0, 6.2.2 and 6.3.0:
- all 10 tree charts render with exactly the vaccine strains' labels styled;
- the checkbox param is declared once, whether `plot()` declares it or their
  `base_chart` does;
- their unstyled output is unchanged.

Also fixes the `plot()` Returns docstring: for a strain on `x` the default
`tree_location="bottom"` puts the chart first and the tree second.

Adds a docs example (H3N2 vaccine strains in bold red behind a checkbox),
`vaccine_axis_labels` in the flu example module, and
tests/test_strain_label_styles.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bump `version` to 0.5.0 and close the `[Unreleased]` section.

This is a minor release rather than a patch: `strain_label_styles` is a new
parameter, and charts with selection parameters now render through the
JSON / HTML / dict (CLI) path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The release step now renames `## [Unreleased]` to the version heading and
leaves no empty section above it. The next change adds a fresh
`## [Unreleased]` when it lands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jbloom
jbloom merged commit 3b58473 into main Oct 30, 2026
3 checks passed
@jbloom
jbloom deleted the strain-label-styles branch October 30, 2026 15:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support styling the labels of a subset of tips, optionally toggled by a bound param

1 participant