Skip to content

0.5.1 merge - #96

Merged
Chenglong Wang (Chenglong-MS) merged 42 commits into
mainfrom
dev
Aug 14, 2026
Merged

0.5.1 merge#96
Chenglong Wang (Chenglong-MS) merged 42 commits into
mainfrom
dev

Conversation

@Chenglong-MS

Copy link
Copy Markdown
Contributor

0.5.1

Y.Lab3 (zl190) and others added 30 commits August 6, 2026 08:40
Port the Calendar Heatmap to the Vega-Lite backend, mirroring the
ECharts implementation (ecCalendarHeatmapDef). ECharts uses a first-class
calendar coordinate system; Vega-Lite has none, but it needs no computed
week/day fields either — timeUnit expresses the GitHub-style grid from a
single date field: yearweek on x (one ordinal column per calendar week),
day on y (Sun–Sat rows, Monday-first to match the ECharts dayLabel), and
sum on color collapses rows sharing a calendar day into one cell.

The upstream ECharts calendar.ts header notes VL "has no first-class
calendar; would fake it with rect + computed week/day fields" — timeUnit
avoids the computed fields, so no core changes are needed.

Scheme handling mirrors the ECharts template's encodingActions: named
Vega-Lite schemes (viridis/blues/greens/reds/oranges/purples) pass
through as scale.scheme; "github" has no built-in Vega-Lite equivalent,
so it resolves to an explicit scale.range (the same low→high ramp the
ECharts template uses).

- register in vlTemplateDefs (Tables & Maps group, next to Heatmap)
- tests: 8 cases (registry, dual-axis timeUnit projection, Monday-first
  row order, per-day sum, count fallback, github range vs named scheme,
  cross-backend parity); assembled spec also verified through vl.compile
- docs: regenerate reference-vegalite.md; move Calendar Heatmap from the
  SKILL.md "ECharts adds" list into the shared template table (+ synced
  bundled asset)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `github` scheme now maps counts through a quantile scale (equal-count
bins → the 5 canonical GitHub buckets) instead of a continuous ramp, so a
Calendar Heatmap with `color.scheme: 'github'` renders the discrete, snapped
levels people recognize from the GitHub contribution graph.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds the second backend for the theme pipeline. Stage 2 (`groundTheme`) is
reused unchanged — no field in `core/theme` moved to fit Plotly, which is the
first evidence that the neutral layer really is neutral.

`plotly/theme.ts` writes `DesignDecisions` onto a `{data, layout}` figure:
surface, typography, axes, marks, series ink, legend, facet chrome and data
labels. Where Plotly cannot do what it is told, it approximates and says so in
`figure._theme.report`.

`assemblePlotly` also renders `chart_spec.title` for the first time; a house's
headline treatment is most of what makes it recognisable, and there was
nothing to treat.

`scripts/plotly-sheet.ts` renders contact sheets through headless Chrome —
plotly.js only runs in a browser — and `scripts/theme-plotly.ts` drives the
lab, r2 and real corpora through it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0de04740-1ac7-48d9-844f-92d4e261c27c
Second and third gap batches from the lab audits.

Plotly places a KPI card, a radar and a map by `domain`, measured against the
paper — growing the top margin does not move them, so the headline printed
straight through the number. Their domains are shortened instead.

The title geometry is now measured rather than guessed: scripting the real
renderer shows `yanchor: 'top'` centres the block on `y`, which is why
multi-line headlines kept losing their first line off the top of the figure.

A waterfall and a candlestick colour by direction, not by series, so the
house's categorical set never reached them; they take its status inks now. A
mirrored axis (a population pyramid) prints unsigned labels, as its own axis
does. Stacked segments keep their labels inside and drop the ones that will
not fit rather than shrinking them. Map bubbles are rescaled into the house's
size range directly, and a map's land, ocean and borders come from the house's
surface.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0de04740-1ac7-48d9-844f-92d4e261c27c
Iteration 4 of the Plotly ThemeSpec work, against all 87 r2 cases x 11
columns. Every case assembles and renders; the gaps were in what the
theme could and could not *name*.

- context and reference traces: a bullet chart's zone bands and target
  tick now carry a `_role`, so they are restated against the house
  surface instead of being painted as series
- point labels pick the channel that holds numbers, so a horizontal
  series no longer prints NaN
- an indicator's delta takes the house status inks, lifted where a dark
  card would swallow the stock green and red
- `layout.polar` is themed at last: grid, domain, tick type, and radial
  labels held straight
- a key is measured against the plot area it wraps in, and a column of
  keys taller than the plot grows the figure instead of being clipped
- date tick format follows the tick *step*, not the axis span
- a filled band's edge takes no dots

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0de04740-1ac7-48d9-844f-92d4e261c27c
Iteration 5, against the sixty-one real-data preview cases. All render;
the gaps were measurements made in the wrong units.

- a tick budget is a count, but a panel has a width: a sixteen-panel
  facet grid keeps only the ticks it has room to print, and every panel
  but the last gives up its edge label so it does not land on its
  neighbour's
- a log axis is ticked by decade, so the budget no longer applies to it
- an annotation printed in a series' colour follows that series to its
  new ink, so a sparkline's average is no longer blue beside a red line
- a sparkline's average rule is `_role: 'reference'`, like a bullet
  chart's target; furniture is skipped by the mark pass, and a dashed
  rule takes the weight of a grid line

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0de04740-1ac7-48d9-844f-92d4e261c27c
Pins what the audit sweeps found by eye: furniture is restated rather
than painted as a series, a target tick keeps its role and its ink,
nothing prints NaN, a banded axis keeps its turned labels only where
straight ones will not fit, a polar plot is themed, and every realize
entry carries a path.

Writing them found one more gap: a legend proxy — an empty bar standing
in for a colour — was still being labelled. The labelling pass now skips
a trace with no numbers in it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0de04740-1ac7-48d9-844f-92d4e261c27c
The Plotly theme work could only be reviewed through offline contact
sheets. Both corpus labs now carry a Vega-Lite | Plotly switch, so the
same 87 synthetic and ~60 real cases can be seen themed in a browser.

The real lab's case filter followed Vega-Lite's template registry with a
comment saying Plotly had no ThemeSpec path. It does now, so the filter
follows the selected backend.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0de04740-1ac7-48d9-844f-92d4e261c27c
The labs clipped charts because `figure._width`/`_height` are written at
instantiate time while `layout.width`/`height` keep growing afterwards — title
wrapping, tall legends, theme chrome. `PlotlyView` sizes an overflow:hidden
wrapper from the former, so anything the layout gained was cut off. 957 of 957
r2 figures mismatched; the RESULT block of assemble.ts now ceils and re-syncs
them. Zero clipped charts across both labs and both backends.

With that fixed, `scripts/theme-vs.ts` renders themed Vega-Lite beside themed
Plotly, one row per house, and the audit found:

- A stacked area was never themed at all. `markFamilies` typed traces by their
  explicit `fill`, but Plotly fills a `stackgroup` implicitly, so every stacked
  area read as a bare line.
- Series-end labels sat at raw values rather than where the trace is drawn once
  stacked. `plottedPositions()` computes the drawn position.
- Stacked fills were translucent; they are now opaque.
- Plotly reverses a stacked legend — `traceorder: 'normal'`.
- Dots on lines were unbounded. `dotDiameter()` caps them and drops them when
  points crowd under 6px apart.
- Bubbles were all one size: the scatter template declared `size` and ignored
  it. `sizeScale()` maps it onto an area.
- Pie labels doubled the legend and stood on end.
- Log axes named their minors; short spans need `dtick: 1`.
- Four Olympic years became "2015, 2020, 2025". `pinSparseTicks()` names the
  values an axis holds when there are few of them — but not on a measure axis,
  unless they are a contiguous integer run (a rank), and `spaceOutTicks()` drops
  any survivor that lands within a label width of its neighbour.
- Data labels printed `12.8k$`; a currency sign leads, a word unit trails.
- The radar shared one radial scale across metrics in different units, burying
  four of five spokes at the centre. Each axis is normalised to its own ceiling
  and that ceiling is written into the spoke label, as the Vega-Lite radar does.
  Where the ceilings already agree nothing is normalised and the radial ticks
  are shown instead.

Stack order, category order and pie palette exhaustion are left alone: they are
template-level and internally consistent.

957 tests pass. Iteration 8 documented in design-docs/07-plotly-theme.md.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0de04740-1ac7-48d9-844f-92d4e261c27c
Radar plots were paying for title and legend space twice: Plotly already lays
polar and other domain subplots inside the margin, then the theme shortened
their domains again. Remove that second reservation, tighten polar-only
margins, and reserve measured spoke-label strips outside the plot so labels do
not consume radius.

Calibrate Plotly's title anchor and line box against the browser, then recompute
the fractional title position after final height growth. This keeps domain
plots and narrow KPI cards clear of multi-line titles.

Distinguish primary point marks from supporting vertices in ThemeSpec with
marks.point.secondarySize. Ground it to min(primary size, 25px²) by default,
tag radar vertices in both templates, and apply the same secondary size in
Plotly and Vega-Lite. Large house scatter dots no longer obscure radar paths.

Treat radar spokes and rings as required coordinate furniture: always present,
mixed quietly from structural ink, capped at 1px, and fixed to four levels with
only the outer maximum labeled. Keep primary scatter sizing unchanged.

958 tests pass. Iteration 9 is documented in design-docs/07-plotly-theme.md.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0de04740-1ac7-48d9-844f-92d4e261c27c
Plotly cycled a house palette when a pie had more slices than indexed inks,
which assigned the same colour to unrelated categories. Match Vega-Lite's
part-to-whole overflow policy instead: rank categories by share, keep the
largest palette-sized tier, sum the tail into one explicit Others (N) slice,
and paint it with the house overflow ink.

Keep the folded trace out of generic per-slice recolouring so the overflow ink
is not replaced by another palette cycle. Report the approximation through the
same ink.series.categorical path used by Vega-Lite.

Add a cross-backend 25-category test pinning NYT's top 12 plus Others (13), the
summed tail value, overflow colour, transform, and report. 960 tests pass.
Iteration 10 is documented in design-docs/07-plotly-theme.md.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0de04740-1ac7-48d9-844f-92d4e261c27c
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0de04740-1ac7-48d9-844f-92d4e261c27c
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0de04740-1ac7-48d9-844f-92d4e261c27c
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0de04740-1ac7-48d9-844f-92d4e261c27c
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0de04740-1ac7-48d9-844f-92d4e261c27c
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0de04740-1ac7-48d9-844f-92d4e261c27c
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0de04740-1ac7-48d9-844f-92d4e261c27c
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0de04740-1ac7-48d9-844f-92d4e261c27c
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0de04740-1ac7-48d9-844f-92d4e261c27c
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0de04740-1ac7-48d9-844f-92d4e261c27c
feat(vegalite): add Calendar Heatmap template
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0de04740-1ac7-48d9-844f-92d4e261c27c
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0de04740-1ac7-48d9-844f-92d4e261c27c
Normalize calendar dates in UTC, preserve sparse week domains, theme count-only cells, and keep aggregate labels readable across sequential ramps.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0de04740-1ac7-48d9-844f-92d4e261c27c
Register a curated calendar example and contain its wide thumbnail so the complete date range remains visible.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0de04740-1ac7-48d9-844f-92d4e261c27c
…abels honest

Each house now states how far a sparse categorical axis should broaden toward
the room it has, as `layout.bandStepFit`:

  datawrapper 1.0   powerbi / powerbi-light 1.0   nyt 0.7   economist 0.55
  mckinsey 0.35     nature 0.2                    swiss 0.5  cartoon 0.75
  pop 0.85

The product houses fill their container because that is what the products do;
the editorial houses keep more of their own rhythm; Nature is a fixed print
figure. Swiss, Cartoon and Pop are Flint design languages rather than replicas
of a chart system, so their values are design choices, not measurements.

Fit is now bounded by an absolute ceiling, `max(template base step, 100px)`,
instead of a share of the canvas. Three categories in a wide plot were taking a
third of it each — a 144px-wide box plot stops reading as a mark and starts
reading as a panel. A template that already asks for a wide band keeps it, so a
slopegraph's two columns are unaffected.

A house may also ask for straight tick labels, and that ask is now refused when
the names are wider than their bands: `2025-01` beside `2025-02` in a 46px band
printed as one number. Vega-Lite gained the guard Plotly already had, and the
layout's settled pitch is carried into the axis pass so it can measure the fit.

Four shared fixtures are updated to the reference the assemblers now produce
(`scale.zero` on a padded banded axis). Python fixture parity is unchanged at
600 passing.
The template writes its x encoding as `ordinal` — a month is a step in a
bridge, not a date — but declared only that the axis was banded. Where the
column resolved temporal, the layout went on sizing x as a continuous axis: no
`width: {step}`, just a fixed span the twelve steps had to share. The two
disagreed, and everything keyed to the band step quietly did nothing to it —
the house's sparse fit had no step to widen, and the label pass had no pitch to
measure names against, which is why `2025-01` ran into `2025-02` on the front
page while the same chart over plain categories was fine.

Declaring the resolved discrete type puts the layout back in step with the
encoding: the front-page waterfall now moves 32 → 42 → 48px across the fit
range and turns its labels where they no longer fit. Mirrored in flint-py,
which had the same gap; both now agree at step 27 for the shared case.
With its own values switched off, the waterfall left no text marks behind, so
the house's label layer took the chart for an unlabelled one and reached for
"the measure" — which on a waterfall is `__wf_prev_sum`, the level each bar
*starts* from. It printed the running total under the bar rather than what the
step was worth: a stray `0` on the opening step and a lone `932k` further
along, most of the rest dropped by the inside/outside thresholds, which is what
made it read as a leak rather than a wrong answer.

A column a template computed to draw itself is not a quantity anyone asked to
read, so the label layer now stands down when the measure it lands on is one.
The existing guard only caught templates that had already drawn text, which is
exactly the case where nothing was wrong.

Heatmaps are untouched: a cell's measure is the real field on `color`, so the
house still prints it in the cell.
`bandStepFitCapacity` handed the fit the whole `canvasSize`, so a banded axis
aimed its bands at room the axes, key and title block had already spent. The
plot came out the full width of the box and everything drawn around it pushed
the chart past its edge: the front-page waterfall asked for 720 and rendered
787.

The reserve is read off presence, not measured — is there a title block, does
the house put its key at the side, is the axis labelled at all — with fixed
allowances behind each. Wrong in the small, and deliberately so: a long name
costs more than a short one, and a facet pays per panel. It is a soft margin
against aiming at room that was never there, not a promise about the last
pixel, and it is floored so furniture can take a chart's margins but never most
of its plot.

A key is reserved wherever the template can produce one, since a template that
builds its own from a working column — a waterfall's step types — shows no
bound colour channel to read.

Front page now fits in all ten houses (787 -> 700, step 48 -> 42). Across the
real-data gallery, charts exceeding their stated ceiling fall 43 -> 39 of 570,
for 7px of median band step. The rest are two classes this does not reach and
did not introduce: facet grids, which pay furniture per panel, and horizontal
bars whose width is set by long category names rather than by band fit.

The straight-label regression now asserts that a refused angle leaves the axis
turned, rather than pinning the exact degree — at the narrower step the house
states no angle and Vega-Lite's own turn applies, which is the same guarantee
by another route.
A calendar ends whenever the data does, and that is mid-week as often as not:
the January–April sample stops on a Tuesday, so the last column has no Wednesday
through Sunday. Vega-Lite's default view border drew a rectangle around the
whole plot, and the only part of it not covered by cells was exactly those five
missing days — reading as an empty box hanging off the end of the grid rather
than as the days a part week does not have. The cells are the grid; the frame
was only ever outlining their absence.

Corner radius becomes a chart property rather than a fixed 2, so the tiles can
be squared into a continuous field where that reads better.
Themes reach a second backend, and a run of sizing work lands behind them.

Plotly now realizes a ThemeSpec. Stage 2 grounding was reused unchanged from
Vega-Lite, which is the first real evidence that the neutral layer is neutral
rather than a Vega-Lite layer with a general name on it. The Plotly templates
were then walked through a paired audit against themed Vega-Lite until the two
agreed on point density, label wrapping, wedge totals and polar guides.

The rest is fit. Bands are fitted per house against a calibrated bandStepFit,
the furniture a chart is about to draw is reserved before those bands are
measured, and a headline too wide for its block is set down a size before it is
broken. Each of those was a case where a chart quietly grew past the canvas the
caller asked for.
The update note points at the changelog rather than a release tag, since 0.5.1
is not being tagged.

The hosted MCP endpoint was documented in the setup tutorial and on the site,
but a reader arriving at the repo had to install something before they could
try anything. It now sits above the fold, next to the two packages it belongs
with.
@Chenglong-MS
Chenglong Wang (Chenglong-MS) merged commit 34ef451 into main Aug 14, 2026
7 checks passed
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.

2 participants