0.5.1 merge - #96
Merged
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
0.5.1