` components render a wrapper div set to `width: 100%; height: 100%`. They fill whatever container you put them in.
-For charts, the container needs an explicit height (charts don't have intrinsic height):
+An explicit height supplies the chart's figure budget. To keep an exact outer height even when annotations produce notes, set `chromeLayout: 'subtract'` on the spec. Without a height constraint, charts use a 400px visualization budget and add chrome:
```tsx
{
@@ -1029,7 +1066,7 @@ For charts, the container needs an explicit height (charts don't have intrinsic
;
{
- /* Won't work: no height constraint, chart collapses to minimum */
+ /* Automatic height: 400px visualization budget plus chrome */
}
@@ -1052,7 +1089,7 @@ createChart(container, spec, { autoResize: false });
### Forcing size for export
-When you need a chart at specific dimensions (for image export, thumbnails, etc.), set the container to the exact size you want, disable responsive, and export:
+For exact export dimensions, set the container size, use `chromeLayout: 'subtract'` to prevent note-band growth, and disable automatic resize:
```ts
const container = document.createElement("div");
@@ -1060,7 +1097,7 @@ container.style.width = "1200px";
container.style.height = "800px";
document.body.appendChild(container);
-const chart = createChart(container, spec, { autoResize: false });
+const chart = createChart(container, { ...spec, chromeLayout: 'subtract' }, { autoResize: false });
const pngBlob = await chart.export("png", { dpi: 2 });
chart.destroy();
@@ -1346,7 +1383,7 @@ import { createTable } from "@opendata-ai/openchart-vanilla";
const table = createTable(container, tableSpec, {
darkMode: "auto",
- responsive: true,
+ autoResize: true,
onRowClick: (row) => console.log(row),
onStateChange: (state) => {
// state: { sort: SortState | null, search: string, page: number }
@@ -1551,7 +1588,7 @@ import { createGraph } from "@opendata-ai/openchart-vanilla";
const graph = createGraph(container, graphSpec, {
darkMode: "auto",
- responsive: true,
+ autoResize: true,
onNodeClick: (node) => console.log("Clicked:", node),
onNodeDoubleClick: (node) => console.log("Double-clicked:", node),
onSelectionChange: (nodeIds) => console.log("Selected:", nodeIds),
diff --git a/docs/migrating-v9.md b/docs/migrating-v9.md
new file mode 100644
index 00000000..b2660ffa
--- /dev/null
+++ b/docs/migrating-v9.md
@@ -0,0 +1,579 @@
+# Migrating to OpenChart v9
+
+v9 makes the annotation layout options introduced in 8.9.1 the default, consolidates theme precedence, and removes the deprecated authoring and editing APIs. Upgrade all six OpenChart packages together. This guide covers the library; OpenStudio's document envelope, migration dialog, and adoption are separate work.
+
+## How to use this guide
+
+If you are upgrading from v7 or earlier, work through [the v8 migration guide](migrating-v8.md) first. This guide starts from v8.9.1. Unlike v8's deprecation period, v9 rejects the retired spec forms listed below; it does not silently rewrite them during compilation.
+
+1. Upgrade the OpenChart packages your app uses to the same 9.x version. Update your lockfile and run TypeScript before changing stored documents.
+2. Run `migrateSpecV8` on a copy of each older spec and review its repairs. Choose whether to preserve v8 styling or adopt the new defaults.
+3. Update callbacks, imports, wrapper props, and host CSS using the sections below. The spec migration cannot change application code.
+4. Review narrow and exact-height embeds, edit/undo flows, and exports before saving migrated documents.
+
+A useful first search in your application source is:
+
+```sh
+rg 'onEditTransaction|onAnnotationEdit|onTextEdit|ElementEdit|onDataPointClick|updateVisuals|footnoteBandHeight|TOP_PAD_NARROW_MAX|LABEL_GAP_NARROW_MAX|oc-table--compact'
+```
+
+Also inspect `responsive` in mount options or component props (every mount family, see [Interaction mode, chart specs, and resize options](#interaction-mode-chart-specs-and-resize-options)), `compact` in tables, `valueFormat` in specialized specs, `Theme` imports, and `labels.offsets`. Those names have valid uses elsewhere, so do not replace them globally.
+
+## Choose how older documents should look
+
+Run the public engine migration on the original authored document, before compiling it:
+
+```ts
+import { migrateSpecV8 } from '@opendata-ai/openchart-engine';
+
+// Default: rewrite retired fields and materialize former v8 styling defaults.
+const preserved = migrateSpecV8(legacyDocument);
+
+// Rewrite retired fields, leaving omitted defaults to v9.
+const adopted = migrateSpecV8(legacyDocument, { styling: 'v9' });
+
+// Present these changes to the author or record them with the document.
+console.table(preserved.repairs);
+const spec = preserved.spec;
+```
+
+The helper is pure and idempotent. Repairs report `path`, `from`, `to`, and `reason`. Explicit authored values, including `false` and `null` opt-outs, remain authoritative. Keep the original document until you have reviewed and saved the result. The helper migrates spec data; it cannot rewrite JavaScript callbacks, TypeScript imports, or host CSS.
+
+The default styling mode writes former annotation weights, subtitle sizes, curve arrows, unlimited wrapping/compact limits, legacy reference anchors, `chromeLayout: 'subtract'`, and v8 theme typography, spacing, and annotation colors. Endpoint controls are materialized at their released v8 values: 13px names, 12px values, 6px entry spacing, 4px name/value spacing and an 18px column gap. `legacyEditorial` remains available for v8 typography alone; it is not a full document migration.
+
+**Keeping v8 styling does not promise pixel-identical v8 layout.** Placement scoring, note numbering, theme composition, rich-text notes, and other v9 layout rules still apply. Review migrated charts at their actual embed sizes. In particular, the legacy series-label rewrite moves labels into the endpoint column and drops manual per-series offsets; each dropped offset appears in `repairs`.
+
+## Value labels, endpoint correctness, and chart craft
+
+Value labels now use the resolved theme font in horizontal bars, vertical columns, lollipop/dot plots and dumbbell ranges. Pie labels already did so. This fixes the former hardcoded system font. Even the default migration cannot preserve that font mismatch independently: there is no value-label-only font-family control. Its theme repair records the limitation when applicable, and repeated migration remains idempotent. To use a system font throughout a chart, author `theme.fonts.family`; this also changes its axes and chrome. Review custom-font charts and exported labels.
+
+Adaptive horizontal bars also check the complete formatted text width before putting a value inside a mark, including at large font sizes. If neither inside nor outside fits, the value stays available in accessible data and tooltips instead of overflowing. Negative column labels now follow the source number even when the formatted value uses a Unicode minus or omits its sign. Endpoint values share precision across the displayed field, measure all formatted values, and honor the host text measurer. Explicit endpoint formatting still wins, followed by the existing channel/axis format. These correctness fixes can change text width and placement in migrated documents. They do not change source observations or units.
+
+Endpoint budgeting now includes both columns, their gaps, date context and the final axis reservations. Automatic endpoints can yield to the existing legend or requested keyed summary to retain a usable plot. Explicit endpoint choices remain authoritative; an impossible fit emits an `onDiagnostic` message at `/endpointLabels`. Rotated y-axis titles also keep their full glyph ascent inside the figure, including with enlarged fonts.
+
+Bar placement and interior names remain opt-in. The approved defaults below change grid/zero hierarchy, stacked-area endpoint anchors, static map keys and map borders. Bar/column proportions, endpoint sizes/gaps and the no-data hatch retain their previous values.
+
+### Bar and column placement
+
+`labels.placement` accepts `'adaptive'`, `'inside'`, `'outside'` or `'column'` on bar marks. Omission uses adaptive placement. Inside labels are hidden when their complete text box cannot fit; their values remain in accessible data and tooltips. Outside labels follow the positive or negative endpoint, with space reserved before scales are computed.
+
+In a short fixed-height figure, outside labels yield to suppression if their reserves would leave less than the usable plot height after axes, chrome, notes and headers. The diagnostic identifies `/labels/placement`; values remain available through tooltips and accessible data.
+
+A value column is for simple horizontal bars with one value per category. It is unsupported on vertical, grouped or stacked bars. It measures the complete formatted values, including affixes, and reserves that width plus 12px. Facets share the measurement and align columns. If it cannot retain a usable plot, the entire chart falls back to outside labels, then suppression if necessary, with an `onDiagnostic` message at `/labels/placement`.
+
+```ts
+{
+ mark: 'bar',
+ data: [{ department: 'Research', budget: 128.4 }, { department: 'Operations', budget: 75.2 }],
+ encoding: {
+ x: { field: 'budget', type: 'quantitative', axis: false },
+ y: { field: 'department', type: 'nominal', axis: { grid: false } },
+ },
+ labels: { placement: 'column', format: '.1f', suffix: 'M' },
+}
+```
+
+Dot/lollipop, range and pie marks reject explicit placement at `/labels/placement`; their existing label configuration remains supported.
+
+### Optional bar row inspection
+
+Pass `{ rowHover: true }` to `createChart` or the `rowHover` prop on any Chart wrapper. Pointer inspection spans the category, plot and value-column zones; the existing keyboard navigation highlights the same row. It applies to simple horizontal bars, and yields to editing. Omission adds no hover presentation. Grouped, stacked, layered and faceted charts retain their existing behavior. The highlight is transient and does not appear in settled chart exports.
+
+### Stacked-area identification
+
+Stacked areas now anchor endpoints at the band midpoint. Interior series names remain opt-in:
+
+```ts
+endpointLabels: { anchor: 'band', placement: 'interior' }
+```
+
+Omitted placement remains `'end'`; the anchor default is now `'band'`. Set `anchor: 'boundary'` to retain cumulative-edge anchors; `migrateSpecV8` materializes this when preserving v8 styling. Band anchoring places the series' own endpoint value at the middle of its compiled band. It leaves the mark's hover/crosshair points unchanged. Interior labels identify series by name; they do not select a new numerical observation. A series without a fitting interior box retains endpoint identification. Missing and synthetic pivot rows do not become observed zero labels, and endpoints on different dates retain date context.
+
+Layers with independent y-scales fall back from interior names to endpoint names with a diagnostic: one independently scaled band can cover another's accepted text box. This choice happens before the shared columns are measured. Band anchors, value formats and leading/trailing settings remain authored, and band leaders remain visible on the assembled chart.
+
+Independent-layer legends now measure the complete eligible series set before the shared frame is fixed. Authored hiding, row and symbol limits remain in force; previously overflowing wrapped keys can reserve more space.
+
+Stacked-area domains now cover the complete bands, including when stacking is omitted and the existing area default applies. Independent scales no longer pin stacked data to raw row extrema. This corrects clipped bands; it can change axes and geometry in older affected charts. `stack: null` or `false` still requests overlapping areas. Endpoint collision placement also separates labels whose natural anchors lie above the visible domain when the label stack fits.
+
+### Horizontal axis-title headers
+
+Use the existing axis vocabulary to put a y-axis title above its top tick:
+
+```ts
+encoding: {
+ y: {
+ field: 'revenue', type: 'quantitative',
+ axis: { title: 'Revenue per customer ($)', titleAngle: 0, titleAnchor: 'end' },
+ },
+}
+```
+
+Rotated y-axis titles also receive enough left-edge clearance for their SVG alphabetic baseline. Previously, large text could clip past the figure edge. This correctness fix can move a migrated title while keeping its plot, ticks and observations unchanged; migration does not restore that clipping.
+
+The engine measures and reserves the header, including wrapping, before scales and placement. Omission keeps the rotated title. This example documents the header combination; inspect other authored title angles at the actual embed size.
+
+### Map legend positions and theme controls
+
+Point category keys now paint in their resolved legend band. v8 could reserve a top band while painting that key at the bottom; omission now correctly paints at the top. Migration does not preserve that misplaced-key bug. Authoring `legend.position: 'bottom'` requests a real bottom reservation, so it can also change map geometry and is not a pixel-identical substitute. Review maps with categorical point keys after migration.
+
+Maps now honor `legend.position: 'right'` and `'bottom-right'` as a measured side gutter. Both previously type-checked but rendered at the top, so documents already setting them change appearance. A side key that cannot fit falls back to the top with a diagnostic. Top/bottom behavior stays available; `'top-left'` retains its existing point-key overlay meaning, and unsupported map `'inline'` warns and uses a top band. Projection proportions and geographic coverage are preserved.
+
+Endpoint typography and spacing are theme controls with the current, released-v8 numerical values:
+
+```ts
+theme: {
+ fonts: { sizes: { endpointLabel: 13, endpointValue: 12 } },
+ spacing: { endpointEntryGap: 6, endpointValueGap: 4, endpointColumnGap: 18 },
+}
+```
+
+Map authors may set `colors.mapNoDataFill`, `mapNoDataHatch`, `mapInteriorBorder` and `mapOutlineBorder`, including light/dark color pairs. Omission uses the quieter v9 borders and the existing map palette and pattern treatment. Keep no-data patterns distinguishable from valid zero and diverging midpoints. These controls do not introduce an automatic crop, drop regions, reorder points or transform measures.
+
+## Changed annotation and figure defaults
+
+**Who is affected:** Charts relying on omitted styling, wrapping, placement, or height options. These are deliberate default changes and do not produce deprecation warnings. Use explicit settings from the table below or the default migration mode to retain the former authored styling.
+
+| Setting | v8 default | v9 default / explicit override |
+| --- | --- | --- |
+| Small and axis tick text | 11px | 12px; set `theme.fonts.sizes.small` / `axisTick` explicitly |
+| Source, byline, footer | 11px | 12px; set `theme.chrome..fontSize` |
+| Chrome-to-chart / chart-to-footer gap | 8px / 8px | 16px / 12px; set the corresponding `theme.spacing` fields |
+| Subtitled callout primary weight | 700 | 400; set `fontWeight: 700` for a bold lede |
+| Subtitle size | `round(fontSize * 0.85)` | `round(fontSize * 12 / 13)`; set `subtitleFontSize` to override |
+| Curve connector arrow | Implied | Off; use `connector: { type: 'curve', arrow: true }` |
+| Labeled reference line | Legacy anchor placement | Automatic placement if neither `labelPlacement` nor `labelAnchor` is authored |
+| Callout wrapping | Unlimited | Up to `min(240, plotWidth - 2 * padding)` pixels, with a positive minimum; `maxWidth: null` disables wrapping limits |
+| Compact inline callout limit | Unlimited | One eligible text callout; `responsive.compactAnnotationLimit: null` removes this count limit |
+| Reference/range label size | 11px | Theme `small`; explicit annotation `fontSize` wins |
+| Reference rule ink | `#666666` in both modes | `#888888` light / `#9ca3af` dark |
+| Range fill | `#000000` | `#f0c040`; opacity behavior is unchanged |
+| Marker/label-plate fill | Fixed white / near-black | Resolved neutral surface, unless explicitly authored |
+
+Callouts with `responsive: false` remain inline and do not count toward the compact limit. Annotations written in `overrides.compact.annotations` are exempt from the default limit; an explicit `responsive.compactAnnotationLimit` still applies to them. The limit uses the **outer container width**, including facets, at the compact breakpoint of 400px. It does not override the existing behavior of chart-level `responsive: false`, `responsive.autoThin: false`, or fixed-size specs: those still hide unpinned annotations at compact widths. Heights below 200px retain tooltip-only behavior.
+
+Automatic placement processes higher-priority annotations first, breaking ties by authored source order. Placement also penalizes connector crossings. Those choices can move labels even when you preserve v8 styling. Explicit offsets remain pinned, including `{ dx: 0, dy: 0 }`.
+
+- A text connector stops a few pixels short of its target's own value label instead of striking through it, including on callouts with explicit offsets (the label stays put). Automatic and anchor-nudged placement also prefer spots whose connector clears other value labels. The endpoint dot still sits on the data point, and a connector with too little length left before the value label is dropped.
+- Default numeric tick labels use the least precision that tells the ticks apart and drop trailing zeros: `3.5 / 4 / 4.5` instead of `3.50 / 4 / 4.50`. An authored `axis.format` or field `format` is unchanged.
+- A default temporal x-axis at compact widths always shows at least two ticks; it no longer collapses to a single date such as `2024`. When the compact tick cap drops the tick that names the year, the kept ticks use full labels (`Jul 2023`, `Apr 2024`) if they fit. Authored `axis.tickCount` and `axis.values` are unchanged.
+- A right-lane outside reference label ends at the chrome padding, aligned with the title, source, and brand, instead of a fixed 16px from the edge. The plot narrows by the difference (1-4px at common sizes).
+- Facet panels cap their top reference lane like single charts: each lane takes at most 25% of its grid row and leaves the panel its 100px minimum, admitting whole rows in source order. Labels that do not fit fall back inside the panel with the usual lane warning. A short grid still grows to its minimum height, measured with the rows that fit at that height.
+
+Omitting `chromeLayout` now grows the figure by its note band while title, subtitle, source, and other chrome retain their existing subtraction behavior. The internal name for this mode is `notes`; it is **not** an authored spec value.
+
+- Use `chromeLayout: 'subtract'` when an embed needs an exact outer height.
+- Use `chromeLayout: 'grow'` to grow by all chrome, including facets.
+- Read `layout.dimensions.height` for the resulting height. Engine-added height is reported in `layout.chrome.grownHeight`; mounts already account for it once.
+
+The note band includes annotation notes and `seriesSummary`. Its reserved source capacity stays stable during annotation drags, so it can contain unused space. Use `layout.chrome.noteBandHeight`; the old core and vanilla `footnoteBandHeight` helpers are removed.
+
+### Outside reference label lanes
+
+**Who is affected:** Charts with `labelPlacement: 'outside'` on vertical reference lines (`x` rules), and charts whose y ticks were planned without a reserved band.
+
+- **Rows pack.** Labels whose horizontal extents don't overlap share a row in the lane above the plot. Packing is greedy in source order, so the result is deterministic. In 8.9.1, each label had its own row, and facet panels still work that way.
+- **The lane is capped.** It takes at most 25% of the requested height, which is the same basis as the 40% chrome cap. A row that doesn't fit is not admitted. Each of its labels falls back to automatic placement inside the plot on its own, and one "exceed their available lane" warning covers the whole compile. A row is reserved whole when admitted, so growing the figure past a row's threshold can shrink the plot by that row's height. The share cap bounds that step; in 8.9.1 an uncapped lane could take a 220px cliff.
+- **The right lane fits its labels.** Horizontal outside labels reserve the width of the widest wrapped label (still capped at 160px or 25% of the width) instead of always reserving the cap, so the plot gets the slack back.
+- **The lane yields before chrome.** When the plot would drop below its minimum, the lane shrinks first. Title and subtitle step down only if the plot still doesn't fit. In 8.9.1 the subtitle could be dropped to keep a label lane.
+- **Tick counts can change.** The layout plan now sizes y ticks against the same margin stack as the final layout: the reference lane, metric bar, series-search band, and (with `chromeLayout: 'subtract'`) the note band. Charts with those bands can get a different number of y ticks than in v8, which planned for a taller plot than it drew and could pack ticks 16px apart. When d3's round steps would leave fewer than three default ticks, v9 asks for a denser set as long as ticks stay at least 20px apart.
+
+### Endpoint markers and curved connectors
+
+**Who is affected:** Line charts with `endpointLabels.showMarker`, and annotations with `connector: 'curve'`.
+
+- Endpoint markers now sit centered on the line's last (or first) point, the same x as hover dots, the crosshair and annotation anchors. In 8.9.1 they were offset by their radius. The default marker fill is the opaque canvas color instead of the theme background, so the ring covers the line end; set `endpointLabels.markerStyle.fill` to keep a transparent ring.
+- A text annotation whose default dot lands on an endpoint marker takes the marker's colors, so the point draws one ring.
+- Curved connectors are shaped to the data point and shortened along their end tangent, so the tail aims at the marker.
+
+### Table surface
+
+Tables paint `theme.colors.background` (transparent by default) like charts do, instead of the mode's opaque `--oc-bg`, so a table sits on the host surface. The header, totals row, search input and pager buttons are transparent too; the opt-in sticky first column keeps `--oc-bg` so scrolled cells don't show through. Inline bar fills render at 40% opacity in dark mode (18% in light). Set a `theme.colors.background` to get an opaque table back.
+
+## Notes and annotation identity
+
+Notes include the complete primary text and subtitle, with wrapping and `**bold**` spans. Numbers are source-order ordinals of text annotations, assigned before filtering and thinning. Gaps are intentional: note 4 remains note 4 when another note is hidden. Inserting, deleting, or reordering authored annotations can renumber them. Facet instances share one source number and note.
+
+Notes and their markers support linked focus/click/tap emphasis. This is transient reader state, not a spec edit, and is excluded from exports. Authored unique annotation IDs remain the recommended durable identity. Missing or duplicate IDs use revision-local source indices and produce diagnostics.
+
+A label drag converts resolved placement into an explicit anchor/offset and sets `responsive: false` in one transaction. The annotation then stays inline on narrow screens; the position is clamped and can overlap other pinned annotations. Changing the data attachment remains a separate operation.
+
+### Editing and keyboard navigation
+
+**Search for:** `onEditTransaction`, `onAnnotationEdit`, `onTextEdit`, `ElementEdit`, and any `onEdit` handler that expects a single legacy element edit. Change the handler to consume the complete transaction, rather than reading the former payload fields.
+
+`onEdit` now receives an `EditTransaction`. The old `ElementEdit` union and the
+additive `onEditTransaction` callback are removed. Apply the complete transaction
+atomically; its inverse supports one undo entry per gesture.
+
+```ts
+// v8 additive API
+createChart(container, spec, { onEditTransaction: persist });
+
+// v9
+import { applyEditTransaction } from '@opendata-ai/openchart-core';
+
+const chart = createChart(container, spec, {
+ onEdit(transaction) {
+ const result = applyEditTransaction(spec, transaction);
+ if (!result.ok) return; // Stale or invalid target: keep the current document.
+ spec = result.spec;
+ undoStack.push(result.inverse);
+ chart.update(spec);
+ },
+});
+```
+
+React uses `onEdit`, Vue uses `@edit` (or `onEdit`), and Svelte uses `onedit`.
+`revision` still cancels an active gesture when the document revision changes.
+Selection and transient visibility remain separate: use `onSelect`, `onDeselect`,
+and `onLegendToggle`; legend toggles do not emit durable transactions.
+
+In edit mode, arrow keys nudge the selected text annotation 1px; Shift+arrow nudges
+10px. Tab/Shift+Tab cycle editable elements; past the last (or first) element
+the selection clears and focus leaves the chart normally. A sequence of nudges
+commits 500ms after the last arrow release, or immediately before selection,
+another gesture, cancellation, or focus leaving the chart. Native text-editing
+keys keep their usual behavior.
+
+Edit mode (`interaction: 'edit'`, or an `onEdit` handler) replaces reader keyboard
+navigation, because arrow keys nudge instead of moving between data points.
+Select mode (`onSelect`/`onDeselect` alone) keeps reader navigation. See
+[Interaction mode](#interaction-mode-chart-specs-and-resize-options).
+
+Removed APIs and their replacements:
+
+| Removed | Replacement |
+| --- | --- |
+| `onAnnotationEdit`, `onTextEdit` | `onEdit` transaction operations |
+| `onDataPointClick` | `onMarkClick` (the old callback had no effect) |
+| Chart mount/wrapper `responsive` option | `autoResize` (spec `responsive` is unchanged) |
+| Graph `updateVisuals()` | `update()` |
+| `ElementRef` series-label variant and `elementRef.seriesLabel()` | Endpoint labels; manual endpoint-label offsets are not supported |
+
+For Chart mounts, move only the resize option:
+
+```ts
+// v8: mount option
+createChart(container, spec, { responsive: false });
+// v9: mount option; spec.responsive keeps its annotation behavior
+createChart(container, spec, { autoResize: false });
+```
+
+Other mount families rename the same option; see
+[Resize option on every mount](#resize-option-on-every-mount).
+
+`applyEditTransaction` validates the whole operation list before returning a new spec. A failed transaction leaves the input unchanged. Annotation `expect` preconditions carry identity/type/text checks, so a stale edit cannot silently target a reordered ID-less annotation. The helper does not compare `revision`; hosts may use it for their own bookkeeping.
+
+## Interaction mode, chart specs, and resize options
+
+**Who is affected:** Hosts that pass `editable`, read `isEditing`, rely on selection callbacks to enable text editing, pass a graph spec to ``/`createChart`, or pass `responsive` to a table, graph, sankey, tilemap, map, or barlist mount.
+
+**Search for:**
+
+```sh
+rg '\beditable[:=]|\beditable=\{|\.isEditing\b|responsive:\s*(true|false)|responsive=\{'
+rg ']*spec=\{[^}]*[Gg]raph|createChart\([^)]*[Gg]raph'
+```
+
+### Interaction mode
+
+The `editable` mount option and wrapper prop are removed. Use `interaction`, one of `'read'`, `'select'`, or `'edit'`:
+
+| Mode | Selection | Edit gestures (drag, nudge, delete, text) | Arrow keys |
+| --- | --- | --- | --- |
+| `'read'` | None, including `select()` and `selectedElement` | None | Reader data navigation |
+| `'select'` | Click and Tab; `onSelect`/`onDeselect` fire | None | Reader data navigation |
+| `'edit'` | Click and Tab | All, emitted through `onEdit` | Nudge the selection |
+
+When `interaction` is omitted it is inferred once per mount: `onEdit` gives `'edit'`, otherwise `onSelect`/`onDeselect` give `'select'`, otherwise `'read'`. Wrappers recreate the chart when `interaction` or the presence of those listeners changes, including Vue's `@select`/`@deselect` (previously read only at setup). `interaction: 'edit'` without `onEdit` warns once, because gestures have nowhere to send transactions. `youDrawIt`, chart stories, and `seriesSearch` suppression all use the resolved mode, so `interaction: 'read'` keeps a youDrawIt chart drawable even when an `onSelect` handler is passed.
+
+**Behavior change:** in v8, selection callbacks alone enabled double-click and Enter text editing. In v9 that is select mode, which never edits; pass `interaction: 'edit'` (with `onEdit`) to keep text editing.
+
+```ts
+// v8
+createChart(container, spec, { editable: false, onEdit, onSelect });
+
+if (chart.isEditing) return;
+
+// v9
+createChart(container, spec, { interaction: 'read', onEdit, onSelect });
+
+if (chart.isTextEditing) return;
+```
+
+`editable: false` maps to `interaction: 'read'`; `editable: true` maps to `interaction: 'edit'`. Vue uses `:interaction="'edit'"`, Svelte uses `interaction="edit"`. `ChartInstance.isEditing` is renamed `isTextEditing`; it still means the inline text editor is open. `InteractionMode` and `resolveInteractionMode` are exported from the vanilla package (the type is also re-exported by each wrapper).
+
+### Chart specs only
+
+`` (React, Vue, Svelte), `useChart`, `createChart`, `ChartInstance.update()`, `beginManualUpdate()`, and chart stories accept `ChartSpec | LayerSpec`. A graph spec passed there already threw at compile time; now TypeScript rejects it. Render graphs with ``/`createGraph`, or `` when the spec type varies.
+
+```tsx
+// v8 (type-checked, failed at runtime)
+
+// v9
+
+```
+
+### Resize option on every mount
+
+Every mount family now names the resize option `autoResize`, matching Chart. Spec-level `responsive` (annotation and chart responsiveness) is unchanged.
+
+```ts
+// v8
+createTable(container, spec, { responsive: false });
+createGraph(container, spec, { responsive: false });
+// v9
+createTable(container, spec, { autoResize: false });
+createGraph(container, spec, { autoResize: false });
+```
+
+This covers `createTable`, `createGraph`, `createSankey`, `createTileMap`, `createGeoMap`, `createBarList`, and the Svelte `useGraph` composable option. The wrappers already observe resizes, so component props are unaffected.
+
+### Element references carry `path`
+
+Every `ElementRef` emitted to `onSelect`, `onDeselect`, and `onAnnotationClick`, and returned by `getSelectedElement()`, now has `path`, including refs from Tab cycling, legend entries, and programmatic `select()` calls that omitted it. Chrome refs use `/chrome/`; legend and legend-entry refs use `/legend`. Hosts that treated a missing `path` as "no authored object" should read `type` instead.
+
+## Theme composition
+
+**Search for:** `Theme` imports, `editorial` / `clarity` preset use, shallow theme spreads, and custom `--oc-*` CSS. Recheck nested overrides: previously discarded leaves now survive theme composition.
+
+Authored theme layers now deep-merge: spec theme, provider theme, then component prop or mount/compile option. A layer resolves two scopes. The root (chrome, axes, legend) uses the layer theme when it has one and the first leaf's theme otherwise, as a whole object, like the layer's other root properties. Each leaf's marks use the leaf theme deep-merged over the layer theme. Host overrides apply on top of both. With independent y-scales, each side's marks and its own axis also use its merged leaf theme. Shared-axis layers match v8, so migration leaves them unchanged. A v8 dual-axis chart ignored leaf themes when the layer set a theme, so migration removes those leaf themes. This preserves unrelated nested fields that a shallow merge previously discarded.
+
+Arrays and `{ light, dark }` color pairs replace atomically. `colors: string[]` remains shorthand for `colors.categorical`. An authored `chrome..fontSize` wins over propagation from the general `fonts.sizes` scale, even when those values originate in different layers. Presets no longer duplicate general title sizes in chrome defaults.
+
+```ts
+import { broadsheet, mergeThemeConfig } from '@opendata-ai/openchart-core';
+
+// Preserve the preset, changing only the rule length.
+const wideRule = mergeThemeConfig(broadsheet, { rule: { width: 80 } });
+// Explicitly remove its rule.
+const noRule = mergeThemeConfig(broadsheet, { rule: null });
+```
+
+`rule` accepts a partial object or `null`. Without a preset rule, omitted width/thickness resolve to 40px/3px and color uses theme text ink. The mount/compile `watermark` option overrides spec and breakpoint values. The graph mount `legend` option also takes precedence over the spec.
+
+Use `ThemeConfig` for authored input and `ResolvedTheme` for compiled output; `Theme` is removed. The empty `editorial` preset is removed: omit `theme` or pass `{}`. `clarity` is deprecated for removal in v10 and now contains only its remaining background, axis, and eyebrow colors. The default background is still transparent; the default axis/eyebrow colors have not adopted clarity's colors.
+
+Only authored theme tokens and their derived values are stamped inline. Omitted tokens stay available to host CSS, including `--oc-border-radius`. Removing an authored override restores the host value. Dark-mode defaults come from generated CSS; authored light/dark pairs still win. SVG/raster export preserves the live CSS token values before leaving the page. See [the token guide](theming-tokens.md).
+
+## Retired spec forms and helpers
+
+Retired spec forms are now validation errors, and removed imports stop compiling. `migrateSpecV8` handles spec rewrites in either styling mode; update imports and application options yourself.
+
+| Removed form | Replacement |
+| --- | --- |
+| Table `compact: true` | `density: 'condensed'`; `false` becomes `'regular'` |
+| `valueFormat` on Sankey/barlist | `encoding.value.format` |
+| `valueFormat` on maps | `encoding.color.format` |
+| Annotation `type: 'rule'` | `type: 'refline'` |
+| Arc/waffle/parliament authoring through `encoding.y` | `encoding.theta` |
+| `theme.chrome.title: '#333'` and other theme chrome strings | `theme.chrome.title: { color: '#333' }` |
+| Spec `darkMode` | Mount/component `darkMode` option |
+| `mark.render` | Mount/component `renderer` option |
+| `encoding.radius`, `shape`, `href`, `order` | Remove them; these channels had no renderer |
+| `animation: 'legacy'` / `animation.preset: 'legacy'` | Use `true` / omit `preset`; omitting animation itself still disables it |
+| `labels.offsets` and legacy line-end series labels (`labels.density` no longer draws anything on line or area marks) | `endpointLabels` |
+| `TOP_PAD_NARROW_MAX`, `LABEL_GAP_NARROW_MAX` imports | `NARROW_VIEWPORT_MAX` |
+
+For example, the migration moves specialized formatting and renames annotation aliases:
+
+```js
+// v8 spec fragments
+{ type: 'barlist', valueFormat: '$,.0f', encoding: { value: { field: 'amount' } } }
+{ annotations: [{ type: 'rule', y: 0, label: 'Baseline' }] }
+
+// v9
+{ type: 'barlist', encoding: { value: { field: 'amount', format: '$,.0f' } } }
+{ annotations: [{ type: 'refline', y: 0, label: 'Baseline' }] }
+```
+
+Move spec `darkMode` to the mount/component yourself: the migration removes the retired spec key and reports the repair, but cannot configure its host. Likewise, move `mark.render` to the host's `renderer` option if you still need that renderer selection.
+
+The spec-level chrome text shorthand **stays**: `chrome: { title: 'A title' }` is valid. Tilemap `valueFormat` also stays, including for record data with no encoding channel. `colors: string[]`, ordinary Vega-Lite sugar, and chart-type value-label configuration remain supported.
+
+Host table styles targeting `.oc-table--compact` must target `.oc-table--condensed` instead. `TableLayout.compact` is removed; read `density`.
+
+For multi-series lines, the traditional legend and endpoint column are now the two series identifiers. If both are explicitly off, the compiler emits an accessibility warning. The old fallback of setting `legend: false`, `endpointLabels: false`, and relying on line-end labels no longer renders those labels. Migration replaces that fallback with:
+
+```ts
+endpointLabels: { content: 'label', showMarker: false }
+```
+
+Series ending before the last x value move to the right-edge column. Manual legacy label offsets are dropped; endpoint-label dragging is not part of v9.
+
+Lines whose series come from `strokeDash` alone (no color encoding) now get the same identification as color series: the endpoint column by default, and otherwise a legend with dashed line swatches, as Vega-Lite builds for `strokeDash`. Both swatches show each series' dash pattern. v8 drew line-end labels on these charts whatever the legend and endpoint settings, so migration writes the endpoint rewrite above for any of them that had labels (`labels.density` not `'none'`). As with color series, that explicit config keeps the column at the compact breakpoint, where v8 drew no labels. `detail` groups without identifying, as in Vega-Lite: it gets no legend, endpoint column, or warning.
+
+## Mark-specific options and field inference
+
+`MarkDef` is now a discriminated union. Options must belong to the authored mark: density `bandwidth`, for example, is not a bar option. Validation checks authored paths before sugar expands histogram/density, and agrees with the published JSON Schema and TypeScript union. Correct the mark or remove the inapplicable option; the migration helper does not guess the intended mark.
+
+A union cannot be extended with an interface. Replace:
+
+```ts
+// v8
+interface AppMark extends MarkDef {
+ editorLabel?: string;
+}
+```
+
+with:
+
+```ts
+// v9
+import type { MarkDef } from '@opendata-ai/openchart-core';
+type AppMark = MarkDef & { editorLabel?: string };
+```
+
+Builders and compilation share one field-inference function. It examines up to 50 rows, ignores nulls, and requires more than 80% agreement. Numeric strings are considered numeric before date parsing, so an untyped column containing `'2020'` is quantitative. Other strings count as dates only when they are date-shaped (ISO dates and date-times such as `'2020-01'` or `'2020-01-01T12:00Z'`, month-name dates such as `'Jan 2020'`, and slash dates) and parse, so labels like `'Product 1'` stay nominal. Set `encoding..type` explicitly when numeric-looking years should be ordinal or temporal. This avoids relying on inference for mixed columns.
+
+A temporal channel whose values are all date-only ISO strings (`'2020'`, `'2020-04'`, `'2020-04-01'`) now gets a UTC time scale. JavaScript parses those strings as UTC midnight, and v8 ticked them in the host's local time, so the first or last date's tick went missing and labels changed with the machine's timezone. The compiled `xInvert.scaleType` / `yInvert.scaleType` for such data reads `'utc'` instead of `'time'`. Data with a time of day, or `Date` objects, keeps the local-time scale; set `scale: { type: 'time' }` to keep local time for date-only strings.
+
+## Validation paths, diagnostic fixes, and annotation labels
+
+**Search for:** `.path` on `validateSpec` errors (string comparisons such as `'encoding.x.field'`, `startsWith('encoding.')`, or code that splits on `.`/`[`), `ValidationResult.warnings`, `markOptionWarnings`, `applySpecPatch`, `SpecPatch`, `.fix.changes`, and `label` on `type: 'text'` annotations.
+
+Every `ValidationError.path` is now a root-relative RFC 6901 JSON Pointer, the same form used by edit transactions, migration repairs, and diagnostics. Keys are escaped (`~` as `~0`, `/` as `~1`) and the root is `''`. Errors inside a layer carry the layer prefix; an error in a value the leaf inherits (shared `data`, or an encoding channel it does not override) points at the parent layer that authored it. `path` is always present. Messages remain prose.
+
+```ts
+// v8
+errors.find((e) => e.path === 'encoding.y.field');
+errors.find((e) => e.path === `data[${i}].${field}`);
+
+// v9
+import { jsonPointer } from '@opendata-ai/openchart-core';
+errors.find((e) => e.path === '/encoding/y/field');
+errors.find((e) => e.path === jsonPointer('data', i, field));
+errors.find((e) => e.path === '/layer/1/encoding/y/field'); // formerly 'encoding.y.field'
+```
+
+The `Invalid spec:` error thrown by compilation prefixes each line with its pointer (`/encoding/y/field: Spec error: ...`) unless the message already names it. `ValidationResult.warnings` was never populated and is removed, as is the internal `compile(spec, { markOptionWarnings })` option.
+
+Diagnostic fixes are edit operations. `SpecPatch.changes` is `EditOp[]`, and `applySpecPatch` applies it with `applyEditTransaction` and returns the inverse:
+
+```ts
+// v8
+// fix.changes: [{ path: '/theme/fonts/sizes/axisTick', value: 12 }]
+spec = applySpecPatch(spec, diagnostic.fix);
+
+// v9
+// fix.changes: [{ op: 'set', path: '/theme/fonts/sizes/axisTick', value: 12 }]
+const { spec: next, inverse } = applySpecPatch(spec, diagnostic.fix);
+undoStack.push(inverse); // applySpecPatch(next, inverse) restores spec
+spec = next;
+```
+
+A fix that must create a missing object uses `add`; `set` requires the parent to exist. Stale or invalid patches still throw. The inverse is guarded by the patched document, so it is rejected after later edits.
+
+`label` belongs only to `range` and `refline` annotations. On a `text` annotation it had no effect and is now a validation error at `/annotations//label`. `migrateSpecV8` removes it and reports the repair. Put the callout copy in `text`, and supporting copy in `subtitle`.
+
+```js
+// v8
+{ type: 'text', x: '2020', y: 12, text: 'Peak', label: 'Peak' }
+// v9
+{ type: 'text', x: '2020', y: 12, text: 'Peak' }
+```
+
+`LayerSpec.responsive` now accepts the same `boolean | ResponsiveConfig` as `ChartSpec.responsive`, matching what validation and migration already wrote.
+
+## Internal compile options
+
+**Search for:** `footnoteReserve`, `footnotePass`, `facetHeightGrown`, `frozenChartArea`, `rightAxisReserve`, and `layered` in options passed to `compileChart`, `compileLayer`, or a mount.
+
+These fields were on the public `CompileOptions` type in v8 but documented as internal: the engine set them when it re-entered its own compile for note bands, facet growth, and dual-axis layers. v9 sizes a chart in one pass, so the re-entry fields are gone and the layer fields moved to an engine-internal type. Remove them from any options object you build. `chromeLayout: 'subtract' | 'grow'` is unchanged.
+
+A custom `measureText` whose note wrapping needs more lines than the reserved note band no longer triggers a second compile. In grow mode, and in the default layout, the figure grows by the extra height. With `chromeLayout: 'subtract'` the figure keeps its size and `onDiagnostic` receives a `layout.note-capacity` diagnostic. Monotone measures, including the built-in heuristic, never hit this case.
+
+## Validate the upgrade
+
+Review exact-height embeds, narrow charts, custom fonts/themes, layers/facets, and exported SVG/PNG. Exercise one edit, undo, cancelled drag, and keyboard nudge with the new transaction handler. Test application persistence against stale annotation identity. Library migration does not upgrade OpenStudio storage or publish packages; those release steps have their own validation.
+
+### Approved chart and map defaults
+
+Ordinary gridlines are quieter, and existing zero rules have stronger contrast. This does not add ticks, expand domains or enable disabled grids. Set `theme.colors.gridline` and `gridlineZero` to override either role. Explicit colors and light/dark pairs remain supported. Bar/column padding and endpoint sizes/gaps are unchanged. When two automatic slope-axis endpoint labels overlap, their text positions now spread apart while the data anchors remain fixed. Explicit tick values retain their authored behavior.
+
+Map interior borders and outlines are quieter in both modes. The no-data hatch and fill remain unchanged: missing observations still differ from valid zero and neutral classes. `migrateSpecV8` restores old grid and border colors where the theme can express them; `{ styling: 'v9' }` adopts the new defaults.
+
+Map keys now reserve measured title/unit/context rows before projection fitting, and size keys choose a less obstructed location using their complete text/circle bounds. These layout changes have no legacy switch; migration reports that limitation rather than claiming pixel parity. Scale values, class boundaries, point sizes and paint order are unchanged. In fixed-height maps, fuller keys leave less height for geography; increase the authored container height when both detailed keys and a large map are required. The library does not crop coverage or move keys automatically to recover that space.
+
+
+### Optional geographic labels
+
+Existing maps do not gain place labels or note bands automatically. Add `encoding.text` or `points.text` for data-driven labels, and `callouts` for authored geographic findings. Point labels and point-anchored callouts require a unique `points.key`. Feature anchors use topology IDs; geographic longitude/latitude and preprojected source coordinates are separate explicit anchor kinds.
+
+Callouts reserve complete note capacity before the map is fitted. Review fixed-height embeds when enabling them; panning and zooming never change that reserve. Hidden inventory text remains available when visual labels collide or are outside their zoom range. Enable `labelInventory: { container }` on a vanilla mount or GeoMap wrapper for a visible host-placed list. This option does not enable reader comparison or annotation editing.
+
+`renderStaticSVG` accepts maps. Use a distinct stable `idPrefix` for each static figure embedded in one document, so clip paths, patterns and descriptions cannot refer to another figure's IDs. Omission retains the existing deterministic standalone-ID behavior. See [geographic labels and callouts](geographic-labels.md) for authoring, camera, accessibility and export details.
+
+### Shared attribution and side map keys
+
+Source, byline, footer and branding now use the same measured footer across all visualization families. The default OpenData mark is 12px. A custom `chrome.brand` replaces it, including in maps, tables and graphs. Long attribution uses the full row and stacks the brand below when that needs less space than a shared row. Explicit text, font size, offsets and `maxLines` still take precedence; an authored offset can intentionally move text beyond its reserved area.
+
+There is no legacy footer-layout switch. `migrateSpecV8` reports this limitation: even with preserved v8 typography, the measured brand reserve can change wrapping and available plot height. Automatic tile labels may become larger when more room is available, and automatic chart connectors may disappear when a label can sit beside its mark. Source observations, domains and units do not change. Review fixed-height embeds and exports after migrating. Compact/hidden chrome retains its existing attribution visibility policy; `watermark: false` removes the default mark and its reserve.
+
+Side map keys now reserve their actual measured content width. Quantitative side keys show explicit range rows using the existing class boundaries, with accessible inclusive/exclusive descriptions. Reserved gutters have no legend plate; overlays keep a background where geography sits behind them. Narrow frames can fall back to the existing top key before projection fitting. These changes preserve coverage and projection proportions; they do not crop geography to enlarge a map.
+
+### Optional reader comparison
+
+The existing four-argument `createReader` observation API is unchanged. To inspect complete date slices, pass a fifth argument with `mode: 'slices'` and a host-owned panel container:
+
+```ts
+const reader = createReader(container, chart, spec, onReaderChange, {
+ mode: 'slices',
+ panel: { container: comparisonHost },
+ comparison: 'absolute',
+});
+```
+
+Slice mode currently supports a single line or area binding; unsupported layered slice readers report a diagnostic. Declare matching field metadata (`measure`, `unit`, `representation`) before expecting numerical comparisons. Date slices use actual transformed observations; sparse series remain missing, and duplicate series/date observations are ambiguous. A third pin replaces B; removing A promotes B. Durable keyed observations reconcile through updates. Replacing unkeyed data or changing the explicit source `revision` clears ephemeral pins; geometry-only updates retain them. `setSelection` is silent; user and data-driven changes include a reason in the callback.
+
+Maps opt in through `createGeoReader`, or the `reader` option on framework GeoMap wrappers. A companion container enables local name search, explicit ranking and pagination. Pins never automatically move the camera: Show on map is a separate action. `colorKey: { activeValue: true, distribution: true }` reserves its complete context before projection fitting. Missing places remain findable and unranked, and distribution counts include only finite joined feature observations represented in the root or an inset, counting duplicated coverage once. Source-only places that the projection cannot display remain searchable without inflating the visible distribution.
+
+Reader pins, preview outlines and HTML comparison panels are transient UI and are excluded from static export. Authored geographic labels and distribution keys are portable and remain in SVG. A snapshot of the comparison readout is intentionally not offered until it has a portable layout; no HTML screenshot or `foreignObject` is embedded. Graph mounts retain their existing export API limitations.
+
+
+### Optional detail geography and inset views
+
+Existing maps keep one root view. To add contextual boundaries without moving the
+primary map, author `geo.detailLayers` with a unique `id`, `features`, and `minZoom`.
+An optional `maxZoom` bounds visibility. Layers use the root view's original fitted
+projection; they do not change joins, values, class boundaries or reader identity.
+`geo.objectKey` selects the primary TopoJSON object and `fitObjectKey` selects its
+fitting geography. Each topology is decoded independently, including its own
+transform. GeoJSON should use the winding expected by the selected D3 projection.
+
+Identity-projection detail requires matching nonempty `sourcePlane` values on the
+primary geography and every detail layer. This is an explicit assertion about
+already decoded coordinates, not a coordinate-system conversion or proof. Source-space insets use identity projection. Mixing
+geographic and source coordinates is rejected before updating the mounted map.
+
+Insets reserve independent labeled frames before the primary fit. For example:
+
+```ts
+geo: {
+ features: states,
+ objectKey: 'states',
+ projection: 'albersUsa',
+ zoom: true,
+ insets: [{
+ id: 'alaska', label: 'Alaska', featureIds: ['02'],
+ placement: 'bottom-left', width: 140,
+ }],
+}
+```
+
+Inset feature IDs refer to primary geography. `removeFromMain` defaults to true;
+set it false to explicitly duplicate coverage. Points are included only through
+explicit `pointIds` matching `points.key`. Shared observations retain one color
+scale and reader identity across views. Inset `featureIds` use topology feature IDs, even when `geo.idField` selects a different property for the data join. Insets disclose their separate scale and
+coverage; impossible frame packing rejects rather than silently dropping a view.
+Choose a projection compatible with the inset geography: not every projection
+produces a useful local fit around an antimeridian or a composite projection clip.
+
+Existing camera methods target the root. Use `getCamera('alaska')`,
+`setCamera(camera, { viewId: 'alaska' })`, and `getCameras()` for independent views.
+Camera callbacks retain the existing first argument and add scope metadata for
+multiview layouts. Geographic anchors can specify `viewId` and `layerId`; unknown
+explicit scopes reject, while a missing feature in a known scope retains its note
+and accessible inventory. Context detail layers currently belong to the root view.
+SVG export preserves every current camera. Keep unique `idPrefix` values for
+multiple static figures; transient reader pins remain excluded.
diff --git a/docs/spec-reference.md b/docs/spec-reference.md
index afd2117f..0b541552 100644
--- a/docs/spec-reference.md
+++ b/docs/spec-reference.md
@@ -68,12 +68,12 @@ The primary input for standard chart types. Source: `core/src/types/spec.ts`.
| `data` | `DataRow[]` | (required) | Array of data rows. Each row is a `Record`. Must be non-empty. |
| `encoding` | `Encoding` | (required) | Maps data fields to visual channels. See [Encoding](#encoding). |
| `chrome` | `Chrome` | `undefined` | Editorial text: title, subtitle, source, byline, footer. See [Chrome](#chrome). |
-| `chromeLayout`| `'subtract' \| 'grow'` | `'subtract'` | How chrome text interacts with the container height budget. See [Chrome layout](#chrome-layout). |
+| `chromeLayout`| `'subtract' \| 'grow'` | note-band growth | How chrome text interacts with the container height budget. See [Chrome layout](#chrome-layout). |
| `annotations` | `Annotation[]` | `undefined` | Text callouts, highlighted ranges, reference lines. See [Annotations](#annotations). |
| `labels` | `LabelConfig` | `undefined` | Data label display controls. See [Labels](#labels). |
-| `responsive` | `boolean` | `true` | Whether the chart adapts to container width via ResizeObserver. |
+| `responsive` | `boolean \| ResponsiveConfig` | `true` | Layout adaptations at compact sizes; mount `autoResize` controls ResizeObserver. |
| `theme` | `ThemeConfig` | `undefined` | Theme overrides. Deep-merged onto the default theme. See [ThemeConfig](#themeconfig). |
-| `watermark` | `boolean` | `true` | Whether to show the tryOpenData.ai watermark. Spec-level value takes precedence over mount/compile options. Defaults to `true`; auto-hides when the container is under 200px tall unless explicitly set. |
+| `watermark` | `boolean` | `true` | Whether to show the tryOpenData.ai watermark. Mount/compile options take precedence over spec and active breakpoint values. Defaults to `true`; auto-hides when the container is under 200px tall unless explicitly set. |
| `animation` | `AnimationSpec`| `undefined` | Animation configuration. `true` enables entrance + update/exit animations. See [Animation](#animation). |
| `description` | `string` | `undefined` | Alt text for the chart (Vega-Lite aligned). Sugar for `a11y.description`. Auto-generated when absent. See the [accessibility guide](accessibility.md). |
| `a11y` | `A11yConfig` | `undefined` | Accessibility overrides: `description` (custom alt text, wins over top-level `description`) and `hidden` (hide from assistive technology). |
@@ -88,6 +88,8 @@ A plain object with string keys. Values can be numbers, strings, dates, nulls, a
### Mark properties
+`MarkDef` is a discriminated union: each object accepts only options for its mark type. TypeScript, JSON Schema, and runtime validation enforce the same contract before sugar expansion. Extend it with an intersection type rather than an interface.
+
The `mark` field on ChartSpec accepts either a string (`'line'`) or an object with additional mark configuration. LayerSpec children are ChartSpecs, so they use `mark` too.
```ts
@@ -187,7 +189,7 @@ Overlay multiple chart types on shared scales. Source: `core/src/types/spec.ts`.
| `annotations` | `Annotation[]` | `undefined` | Annotations on the layered view. |
| `labels` | `LabelConfig` | `undefined` | Label display configuration. |
| `legend` | `LegendConfig` | `undefined` | Legend display configuration. |
-| `responsive` | `boolean` | `true` | Whether the chart adapts to container width. |
+| `responsive` | `boolean \| ResponsiveConfig` | `undefined` | Same as `ChartSpec.responsive`. When omitted, the first leaf's value applies. |
| `theme` | `ThemeConfig` | `undefined` | Theme overrides. |
| `watermark` | `boolean` | `true` | Whether to show the watermark. Defaults to `true`; auto-hides when the container is under 200px tall unless explicitly set. |
| `resolve` | `ResolveConfig` | `undefined` | Resolution strategy for shared vs. independent scales/axes/legends. |
@@ -234,6 +236,7 @@ interface Encoding {
y?: EncodingChannel;
color?: EncodingChannel;
size?: EncodingChannel;
+ theta?: EncodingChannel; // arc/waffle/parliament quantitative value
detail?: EncodingChannel;
facet?: FacetChannel; // small multiples -- see Faceting
row?: FacetChannel; // vertical facet stack -- see Faceting
@@ -311,14 +314,14 @@ The engine validates encoding channels at runtime using `MARK_ENCODING_RULES`. S
| `area` | temporal, ordinal (req) | quantitative (req) | nominal, ordinal (opt) | quantitative (opt) | nominal (opt) | Same encoding as line, filled region |
| `bar` | quantitative (req) | nominal, ordinal (req) | nominal, ordinal (opt) | quantitative (opt) | nominal (opt) | Horizontal bars. x = values, y = categories |
| `column` | nominal, ordinal, temporal (req) | quantitative (req) | nominal, ordinal (opt) | quantitative (opt) | nominal (opt) | Vertical bars. x = categories, y = values |
-| `pie` | -- (opt, unused) | quantitative (req) | nominal, ordinal (req) | quantitative (opt) | nominal (opt) | color = slice categories, y = slice values |
-| `donut` | -- (opt, unused) | quantitative (req) | nominal, ordinal (req) | quantitative (opt) | nominal (opt) | Same as pie with inner radius |
+| `pie` | -- (opt, unused) | -- (use theta) | nominal, ordinal (req) | quantitative (opt) | nominal (opt) | color = slice categories, theta = slice values |
+| `donut` | -- (opt, unused) | -- (use theta) | nominal, ordinal (req) | quantitative (opt) | nominal (opt) | theta = slice values; same as pie with inner radius |
| `dot` | quantitative (req) | nominal, ordinal (req) | nominal, ordinal (opt) | quantitative (opt) | nominal (opt) | Dot plot. x = values, y = categories |
| `scatter` | quantitative (req) | quantitative (req) | nominal, ordinal (opt) | quantitative (opt) | nominal (opt) | Both axes quantitative. size creates bubble chart |
| `beeswarm` | quantitative or nominal (opt) | quantitative or nominal (opt) | nominal, ordinal, quantitative (opt) | quantitative (opt) | nominal (opt) | One position is the quantitative value axis; the other (nominal/ordinal) splits into grouped lanes |
| `range` | quantitative or nominal (req) | quantitative or nominal (req) | nominal, ordinal (opt) | -- | nominal (opt) | Dumbbell/arrow/range-bar. x+x2 (horizontal) or y+y2 (vertical); orientation-dependent second endpoint |
-| `waffle` | -- (opt, unused) | quantitative (req, via `theta`) | nominal, ordinal (req) | -- | nominal (opt) | Unit grid. `theta` (alias for y) = share, color = category |
-| `parliament` | -- (opt, unused) | quantitative (req, via `theta`) | nominal, ordinal (req) | -- | nominal (opt) | Hemicycle seats. `theta` (alias for y) = seat count, color = party |
+| `waffle` | -- (opt, unused) | -- (use theta) | nominal, ordinal (req) | -- | nominal (opt) | Unit grid. `theta` = share, color = category |
+| `parliament` | -- (opt, unused) | -- (use theta) | nominal, ordinal (req) | -- | nominal (opt) | Hemicycle seats. `theta` = seat count, color = party |
| `histogram` | quantitative (req) | quantitative (opt) | nominal, ordinal (opt) | quantitative (opt) | nominal (opt) | Raw values on x; y defaults to the per-bin count. Color groups overlap rather than dodge |
| `density` | quantitative (req) | -- (computed) | nominal, ordinal (opt) | -- | nominal (opt) | KDE over raw x values; y is the estimated density |
| `calendar` | temporal (req) | -- (not allowed) | quantitative (req) | -- | nominal (opt) | Calendar heatmap. x = daily date, color = per-day value; owns its own geometry |
@@ -329,6 +332,8 @@ The engine validates encoding channels at runtime using `MARK_ENCODING_RULES`. S
**y channel**: Vertical position. Quantitative values for most types. Nominal/ordinal categories for bar and dot (horizontal layout).
+**theta channel**: Quantitative part-to-whole values for arc, waffle, and parliament. Their former authored `y` alias is rejected; use `theta`.
+
**color channel**: Series differentiation. Assigns colors from the categorical palette. Required for pie/donut (defines slices). Optional for all others (creates multi-series with legend).
**size channel**: Mark size. Creates bubble charts when applied to scatter. Maps a quantitative field to the dot **radius in pixels**, area-proportionally (sqrt curve); the default range is [3, 30] and `scale.range` overrides it in radius px. A constant lives at `mark.size`, also a radius (default 5, typical 2-12). Note this differs from Vega-Lite, where point `size` is an **area** in px² — a VL size of 110 is `mark.size: 6` here (r = √(area/π)); validation rejects radii above 50 with the conversion.
@@ -422,12 +427,12 @@ Each field accepts either a plain string or a `ChromeText` object for style over
| Field | Position | Default style |
| ---------- | ------------------------------- | ------------------------------------------------------------ |
-| `eyebrow` | Above title | 11px, semibold, uppercase, theme accent — leading accent dot |
-| `title` | Top, above chart | 22px, bold (700), `#333333` |
-| `subtitle` | Below title | 15px, normal (400), `#666666` |
-| `source` | Below chart area | 12px, normal (400), `#999999` |
-| `byline` | Below source | 12px, normal (400), `#999999` |
-| `footer` | Below byline | 12px, normal (400), `#999999` |
+| `eyebrow` | Above title | 11px, medium (500), uppercase, theme accent — leading accent dot |
+| `title` | Top, above chart | 26px, semibold (600), `#09090b` |
+| `subtitle` | Below title | 14px, normal (400), `#71717a` |
+| `source` | Below chart area | 12px, normal (400), `#71717a` |
+| `byline` | Below source | 12px, normal (400), `#71717a` |
+| `footer` | Below byline | 12px, normal (400), `#71717a` |
| `brand` | Right-aligned on the source row | 12px, suppresses the default watermark |
**KPI metric row** is a separate top-level field on chart specs, not part of `Chrome`. Use `metrics: Metric[]` on the chart spec for a horizontal row of label+value cells rendered between the subtitle and the chart area. Each cell can carry a delta and a secondary value. See `Metric` in `core/src/types/spec.ts` for the full shape.
@@ -481,7 +486,8 @@ The top-level `chromeLayout` property controls how chrome text height interacts
| Value | Behavior |
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
-| `'subtract'` | Default. The container height is fixed and chrome text (title, subtitle, source) is subtracted from it, shrinking the plot. Historical behavior. |
+| omitted | Notes and the series summary grow the figure; title, subtitle, source, and other chrome subtract from the container height budget. |
+| `'subtract'` | Keep the exact outer height; all chrome and notes subtract from it, shrinking the plot. |
| `'grow'` | The container height is treated as the plot budget. The rendered SVG grows taller by the measured chrome height, so the plot keeps its full budget (minus axis and padding overhead) no matter how many lines the title wraps to. |
Reach for `'grow'` on fixed-height article or blog charts where a long title on a narrow phone must not compress the plot.
@@ -496,7 +502,7 @@ Reach for `'grow'` on fixed-height article or blog charts where a long title on
}
```
-`chromeLayout: 'grow'` is a no-op for faceted (small-multiples) specs in this version and falls back to `'subtract'`. It is also honored by bar list, sankey, map, and tilemap specs via the spec or compile option.
+`chromeLayout: 'grow'` supports faceted charts and layers; engine-added height is counted once. Read `layout.dimensions.height` for the final height and `layout.chrome.grownHeight` for the added height. The default note-growth policy is internal: `'notes'` is not a public spec value. Bar list, Sankey, map, and tilemap also honor the public spec/compile policies.
---
@@ -533,6 +539,8 @@ Carrying both bin edges matters: the end edge goes to `x2`, which is what puts t
Data-positioned overlays. Source: `core/src/types/spec.ts`.
+Use unique annotation `id` values for durable selection and editing. Notes preserve complete primary/subtitle text and rich spans; their numbers follow source text-annotation order before filtering, so gaps are intentional. Notes and markers support linked focus and click/tap emphasis.
+
Three annotation types are available, discriminated by the `type` field:
### TextAnnotation
@@ -546,15 +554,16 @@ A callout label positioned at a data coordinate.
| `y` | `string \| number` | (required) | Y-axis data value or position. |
| `text` | `string` | (required) | The annotation text. Supports `\n` for line breaks and `**bold**` spans. |
| `subtitle` | `string` | `undefined` | Optional muted second-tone text rendered below `text`. Use for supporting context (methodology, source, etc.). Supports `**bold**` too. |
-| `maxWidth` | `number \| null` | unlimited | Optional positive wrapping width in pixels; null disables wrapping. |
-| `subtitleFontSize` | `number` | `round(fontSize * 0.85)` | Optional positive subtitle size in pixels. |
+| `maxWidth` | `number \| null` | `min(240, plotWidth - 2 * padding)`, at least 1px | Positive display wrapping width in pixels; null disables wrapping. |
+| `subtitleFontSize` | `number` | `round(fontSize * 12 / 13)` | Optional positive subtitle size in pixels. |
| `dot` | `boolean \| AnnotationDot` | see below | Open-ring marker on the data point. Appears by default on non-arrowed connectors. `false` for none, an object to restyle. |
-| `label` | `string` | `undefined` | Additional label text. |
| `fontSize` | `number` | `13` | Font size override in pixels. |
-| `fontWeight` | `number` | `400` (`700` with a `subtitle`) | Font weight override. |
+| `priority` | `number` | source order | Lower values get placement and compact-inline preference; ties retain source order. |
+| `responsive` | `boolean` | `true` | `false` pins the annotation inline and exempts it from thinning/count limits. |
+| `fontWeight` | `number` | `400` | Font weight override. |
| `offset` | `AnnotationOffset` | `undefined` | Pixel offset from computed position. `{ dx?: number, dy?: number }`. |
| `anchor` | `AnnotationAnchor` | `'auto'` | Label placement direction: `'top'`, `'bottom'`, `'left'`, `'right'`, `'auto'`. |
-| `connector` | `boolean \| ConnectorType \| ConnectorConfig` | `true` | `'straight'`, `'curve'`, or `'drop-line'`; `{ type, arrow? }` for explicit arrow control; `false` disables it. |
+| `connector` | `boolean \| ConnectorType \| ConnectorConfig` | `true` | `'straight'`, `'curve'`, or `'drop-line'`; `{ type, arrow?, bend? }` for explicit control; `false` disables it. |
| `connectorOffset` | `{ from?, to? }` | `undefined` | Per-endpoint pixel offsets for the connector line. |
| `background` | `string \| true` | `undefined` | Background plate behind the text, for readability over chart lines. `true` follows the theme surface. |
| `fill` | `string` | theme `annotationFill` | Fill color. |
@@ -562,7 +571,7 @@ A callout label positioned at a data coordinate.
| `opacity` | `number` | `1` | Opacity (0 to 1). |
| `zIndex` | `number` | `0` | Render ordering. Higher values render on top. |
-#### Text: line breaks, bold spans, and the lede rule
+#### Text: wrapping, line breaks, and bold spans
Curve connectors accept optional `bend: -1..1`; omitted or zero preserves the
existing automatic curve. Other connector types ignore `bend` with a warning.
@@ -578,14 +587,14 @@ An authored `offset` object pins placement even when both values are zero.
Only matched pairs turn bold. An unmatched `**` (and an empty `****`) renders literally, so specs with stray asterisks aren't mangled. Bold spans are measured at weight 700, so bounds, collision nudging, and connector exits all account for them.
-When a `subtitle` is present and you set no `fontWeight`, the primary text resolves to weight 700 and the subtitle stays 400. That's the lede + context stack ("Feb. 25" over "2015 maximum"). Setting `fontWeight` explicitly opts out.
+Primary text and subtitles default to weight 400. Set `fontWeight: 700` for a bold primary line or use `**bold**` for selective emphasis; adding a subtitle does not change the primary weight.
#### Connectors
-The connector leaves the text block (primary text plus subtitle, treated as one box) on the side facing the data point, with a 6px standoff gap, and stops short of the marker at the other end. Two voices:
+The connector leaves the text block (primary text plus subtitle, treated as one box) on the side facing the data point, with a 6px standoff gap, and stops short of the marker at the other end. A curved connector is shaped to the data point and then pulled back along its end tangent, so the tail still aims at the marker. Two voices:
-- **Emphasis.** An arrowed connector (`'curve'` defaults to `arrow: true`) is drawn in the label's text ink with a small open-V arrowhead, so the gesture reads as part of the sentence.
-- **Quiet.** A non-arrowed straight leader or a `'drop-line'` is a gray hairline. It points without competing with the words.
+- **Emphasis.** An explicitly arrowed connector (`{ type: 'curve', arrow: true }`) is drawn in the label's text ink with a small open-V arrowhead, so the gesture reads as part of the sentence.
+- **Quiet.** A non-arrowed curve, straight leader, or `'drop-line'` is a gray hairline. It points without competing with the words.
Connectors are dropped when they'd be noise rather than signal: a leader shorter than 8px, or one whose data point sits inside the text block. The marker still renders. A default annotation sits far enough off its point (28px) to draw a real leader without any `offset` authoring; you only lose the line by pulling the label back onto its own marker.
@@ -594,11 +603,11 @@ Connectors are dropped when they'd be noise rather than signal: a leader shorter
| Field | Type | Default | Description |
| ------------- | -------- | --------------------------- | --------------------------------- |
| `radius` | `number` | `4` | Circle radius in pixels. |
-| `fill` | `string` | theme background | Interior fill (the "open ring"). |
+| `fill` | `string` | theme `annotationMarkerFill` (neutral surface by default) | Interior fill (the "open ring"). |
| `stroke` | `string` | the connector's stroke | Ring color. |
| `strokeWidth` | `number` | `1.5` | Ring width in pixels. |
-The marker sits on the data point itself, not on the pulled-back connector tip. You get one by default when a connector is enabled and carries no arrowhead, including drop-lines. An arrowed connector gets no marker unless you ask for one, since the arrowhead already marks the spot. `dot: false` is always bare; an explicit `dot` always wins.
+The marker sits on the data point itself, not on the pulled-back connector tip. You get one by default when a connector is enabled and carries no arrowhead, including drop-lines. An arrowed connector gets no marker unless you ask for one, since the arrowhead already marks the spot. `dot: false` is always bare; an explicit `dot` always wins. A default dot that lands on a line's endpoint marker takes the marker's fill and stroke, so the two draw as one ring.
### RangeAnnotation
@@ -615,9 +624,9 @@ A highlighted band or rectangle.
| `label` | `string` | `undefined` | Range label text. |
| `labelOffset` | `AnnotationOffset` | `undefined` | Pixel offset for the label. `{ dx?: number, dy?: number }`. |
| `labelAnchor` | `AnnotationAnchor` | `'auto'` | Label placement direction. |
-| `fontSize` | `number` | `11` | Font size override for the range label (px). |
+| `fontSize` | `number` | theme `small` (12px) | Font size override for the range label (px). |
| `fontWeight` | `number` | `500` | Font weight override for the range label. |
-| `fill` | `string` | theme `annotationFill` | Fill color. |
+| `fill` | `string` | theme `annotationRangeFill` (`#f0c040`) | Fill color. |
| `stroke` | `string` | `undefined` | Stroke color. |
| `opacity` | `number` | `1` | Opacity (0 to 1). |
| `zIndex` | `number` | `0` | Render ordering. |
@@ -642,16 +651,18 @@ A horizontal or vertical reference line.
| `strokeWidth` | `number` | `1` | Line width in pixels. |
| `labelOffset` | `AnnotationOffset` | `undefined` | Pixel offset for the label. |
| `labelAnchor` | `AnnotationAnchor` | `'auto'` | Label placement direction. |
-| `labelPlacement` | `'auto' \| 'start' \| 'end' \| 'outside'` | legacy anchor behavior | Opt-in screen-space end selection, automatic containment, or a reserved outside gutter/lane. `labelAnchor` selects the side; an authored offset pins the base. |
-| `fontSize` | `number` | `11` | Font size override for the label (px). |
+| `labelPlacement` | `'auto' \| 'start' \| 'end' \| 'outside'` | `'auto'` unless `labelAnchor` is authored | Screen-space end selection, automatic containment, or a reserved outside gutter/lane. `labelAnchor` selects the side; an authored offset pins the base. |
+| `fontSize` | `number` | theme `small` (12px) | Font size override for the label (px). |
| `fontWeight` | `number` | `400` | Font weight override for the label. |
| `fill` | `string` | `undefined` | Fill color. |
-| `stroke` | `string` | theme `annotationText` | Stroke/line color. |
+| `stroke` | `string` | theme `annotationRule` | Stroke/line color. |
| `opacity` | `number` | `1` | Opacity (0 to 1). |
| `zIndex` | `number` | `0` | Render ordering. |
Provide `x` for a vertical line or `y` for a horizontal line. Both can be set for a crosshair.
+With `labelPlacement: 'outside'`, horizontal lines reserve a right lane as wide as the widest wrapped label (capped at 160px or 25% of the width). Vertical lines share a lane above the plot: labels whose extents don't overlap pack into one row, and whole rows are admitted while the lane stays within 25% of the height. A label whose row doesn't fit falls back to automatic placement inside the plot with a warning. The lane shrinks before the title or subtitle does.
+
### Annotation example
```ts
@@ -920,124 +931,80 @@ For line/area marks, mid-morph interruption uses a crossfade from the frozen int
## ThemeConfig
-User-facing theme overrides. Source: `core/src/types/spec.ts`.
+Public authored theme overrides live in `core/src/types/spec.ts`. Every field is optional. Use `ThemeConfig` for input and `ResolvedTheme` for compiled output; the former public `Theme` alias is removed.
-All fields are optional. The engine deep-merges these onto `DEFAULT_THEME`. You only specify what you want to change.
+`mergeThemeConfig()` deep-merges authored layers before `resolveTheme()` supplies defaults. Component theme props override provider themes; mount/compile options override the spec. In a `LayerSpec`, chrome, axes and legend use the layer theme (falling back to the first leaf's); each leaf's marks use its theme merged over the layer theme. Nested objects merge, arrays and `{ light, dark }` TokenValues replace atomically, and null replaces. `colors: string[]` is shorthand for `colors.categorical`.
```ts
-interface ThemeConfig {
- colors?: {
- categorical?: string[];
- categoricalFill?: string[];
- sequential?: Record;
- diverging?: Record;
- background?: string;
- text?: string;
- gridline?: string;
- hairline?: string;
- axis?: string;
- };
- fonts?: {
- family?: string;
- mono?: string;
- };
- spacing?: {
- padding?: number;
- chromeGap?: number;
- };
- rule?: { color: TokenValue; width: number; thickness: number };
- borderRadius?: number;
-}
-```
-
-### Color overrides
-
-| Field | Type | Default | Description |
-| -------------------- | -------------------------- | --------------------------------- | ----------------------------------------------------------------- |
-| `colors.categorical` | `string[]` | 8-color palette | Categorical palette for nominal data. Array of CSS color strings. |
-| `colors.categoricalFill` | `string[]` | copied from `categorical` | Quieter palette used for area-filling marks (bar, area, arc, waffle, calendar, rect). Set it only to control fills and strokes independently; leaving it unset copies `categorical` so a custom palette never drifts between the two. |
-| `colors.sequential` | `Record` | `{ blue, green, orange, purple }` | Sequential palettes keyed by name. |
-| `colors.diverging` | `Record` | `{ redBlue, brownTeal }` | Diverging palettes keyed by name. |
-| `colors.background` | `string` | `'#ffffff'` | Visualization background color. |
-| `colors.text` | `string` | `'#1d1d1d'` | Default text color. |
-| `colors.gridline` | `string` | `'#e8e8e8'` | Gridline color. |
-| `colors.hairline` | `string` | `'rgba(0,0,0,0.14)'` | Structural line color for axis lines and separators. Distinct from `gridline`, which is lighter. |
-| `colors.axis` | `string` | `'#888888'` | Axis line and tick color. |
+import { broadsheet, mergeThemeConfig } from '@opendata-ai/openchart-core';
+import type { ThemeConfig } from '@opendata-ai/openchart-core';
-### Font overrides
+const theme: ThemeConfig = mergeThemeConfig(broadsheet, {
+ fonts: { sizes: { small: 12, axisTick: 12 } },
+ chrome: { title: { fontSize: 28 } },
+ rule: { width: 80 },
+});
+```
-| Field | Type | Default | Description |
-| -------------- | -------- | ----------------------------------------- | ----------------------------------- |
-| `fonts.family` | `string` | `'Inter, -apple-system, ..., sans-serif'` | Primary font family. |
-| `fonts.mono` | `string` | `'"JetBrains Mono", ..., monospace'` | Monospace font for tabular numbers. |
+Authored `chrome..fontSize` takes precedence over propagation from `fonts.sizes`, including across theme layers. Theme chrome values are objects such as `{ color: '#333' }`; string color shorthand is removed. Spec chrome text shorthand (`chrome: { title: 'A title' }`) remains valid.
-### Spacing overrides
+### Color overrides
-| Field | Type | Default | Description |
-| ------------------- | -------- | ------- | ---------------------------------------- |
-| `spacing.padding` | `number` | `12` | Padding inside the chart container (px). |
-| `spacing.chromeGap` | `number` | `4` | Gap between chrome elements (px). |
+Single colors accept `TokenValue` (`string | { light: string; dark: string }`). Palette arrays contain color strings. These are light-mode defaults unless a pair is shown:
+
+| Field | Default | Description |
+|---|---|---|
+| `colors.categorical` | Six-hue palette | Nominal-series stroke palette |
+| `colors.categoricalFill` | Default fill palette; inherits an authored categorical palette when omitted | Quieter fill colors for area-filling marks |
+| `colors.sequential` / `diverging` | Named palette maps | Numeric color scales |
+| `colors.background` | `transparent` | Visualization background; opaque surfaces derive from the neutral surface |
+| `colors.text` | `#09090b` | Main text ink |
+| `colors.gridline` | `rgba(0,0,0,0.08)` | Grid lines |
+| `colors.hairline` | `rgba(0,0,0,0.14)` | Structural separators |
+| `colors.axis` | `#71717a` | Axis lines and tick text |
+| `colors.annotationLabel` | `#333333` | Primary callout ink |
+| `colors.annotationMuted` | `#6b7280` | Subtitle and note ink |
+| `colors.annotationLeader` | `#8f8f8f` | Quiet connector ink |
+| `colors.annotationRule` | `#888888` / `#9ca3af` | Reference line ink, light/dark |
+| `colors.annotationRangeFill` | `#f0c040` | Range fill before opacity |
+| `colors.annotationMarkerFill` / `annotationPlate` | Resolved neutral surface | Marker interior and label plate; explicit colors win |
+
+Generic `annotationFill` and `annotationText` retain their other uses. See [Theming tokens](theming-tokens.md) for the complete token and dark-mode mapping.
+
+### Fonts, chrome, and spacing
+
+| Field | Default | Description |
+|---|---|---|
+| `fonts.family` | Inter Variable, Inter, system sans-serif | Primary font family |
+| `fonts.mono` | JetBrains Mono, Fira Code, Cascadia Code, monospace | Tabular/monospace family |
+| `fonts.sizes.title` / `subtitle` | 26 / 14 | General title sizes |
+| `fonts.sizes.body` / `small` / `axisTick` | 13 / 12 / 12 | Body, small, and tick sizes |
+| `fonts.sizes.metricLabel` / `metricValue` | 11 / 22 | Metric row sizes |
+| `fonts.weights.normal` / `medium` / `semibold` / `bold` | 400 / 500 / 600 / 700 | Weight ladder |
+| `chrome.eyebrow` | 11px, 500, `#06b6d4`, 1.4 line-height | Eyebrow style |
+| `chrome.title` | 26px, 600, `#09090b`, 1.15 line-height | Title style |
+| `chrome.subtitle` | 14px, 400, `#71717a`, 1.45 line-height | Subtitle style |
+| `chrome.source` / `byline` / `footer` | 12px, 400, `#71717a`, 1.4 line-height | Attribution styles |
+| `spacing.padding` / `chromeGap` | 20 / 6 | Container padding / gap between chrome elements |
+| `spacing.chromeToChart` / `chartToFooter` | 16 / 12 | Gaps above and below the chart |
+| `spacing.axisMargin` | 6 | Axis margin |
+| `borderRadius` | 8 | Containers and tooltips |
### The editorial rule
-`rule` draws a short colored bar above the chrome block — the Economist/FT
-masthead device. Omit it (the default on every preset but `broadsheet`) for no
-rule.
+`rule` draws a short colored bar above top chrome. Omission inherits a preset rule, or draws none on the default theme. `rule: null` removes an inherited rule; partial objects preserve unmodified fields.
-```ts
-theme: {
- rule: { color: { light: '#e3120b', dark: '#f4463f' }, width: 40, thickness: 3 },
-}
-```
-
-| Field | Type | Default | Description |
-| ---------------- | ------------ | ----------- | ------------------------------------------------------------------ |
-| `rule.color` | `TokenValue` | (required) | Rule color. A `TokenValue` accepts a `{ light, dark }` pair, so it needs no dark-mode branch. |
-| `rule.width` | `number` | (required) | Rule length in pixels. |
-| `rule.thickness` | `number` | (required) | Rule height in pixels. `computeChrome` reserves `thickness + chromeGap` above the eyebrow/title. |
+| Field | Fallback when a rule is authored without a preset | Description |
+|---|---|---|
+| `rule.color` | Theme text ink | TokenValue color, including light/dark pairs |
+| `rule.width` | 40px | Rule length |
+| `rule.thickness` | 3px | Rule height; chrome reserves thickness plus chromeGap |
No top chrome means no rule.
-### Other
-
-| Field | Type | Default | Description |
-| -------------- | -------- | ------- | -------------------------------------------------------- |
-| `borderRadius` | `number` | `4` | Border radius for the chart container and tooltips (px). |
-
-### DEFAULT_THEME reference
-
-The full default theme (from `core/src/theme/defaults.ts`):
-
-| Property | Value |
-| ------------------------ | ------------------------------------------------------------------------ |
-| `colors.background` | `#ffffff` |
-| `colors.text` | `#1d1d1d` |
-| `colors.gridline` | `#e8e8e8` |
-| `colors.axis` | `#888888` |
-| `colors.annotationFill` | `rgba(0,0,0,0.04)` |
-| `colors.annotationText` | `#555555` |
-| `fonts.family` | Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif |
-| `fonts.mono` | "JetBrains Mono", "Fira Code", "Cascadia Code", monospace |
-| `fonts.sizes.title` | 22 |
-| `fonts.sizes.subtitle` | 15 |
-| `fonts.sizes.body` | 13 |
-| `fonts.sizes.small` | 11 |
-| `fonts.sizes.axisTick` | 11 |
-| `fonts.weights.normal` | 400 |
-| `fonts.weights.medium` | 500 |
-| `fonts.weights.semibold` | 600 |
-| `fonts.weights.bold` | 700 |
-| `spacing.padding` | 12 |
-| `spacing.chromeGap` | 4 |
-| `spacing.chromeToChart` | 8 |
-| `spacing.chartToFooter` | 8 |
-| `spacing.axisMargin` | 6 |
-| `borderRadius` | 4 |
-| `chrome.title` | 22px, 700 weight, `#333333`, 1.3 line-height |
-| `chrome.subtitle` | 15px, 400 weight, `#666666`, 1.4 line-height |
-| `chrome.source` | 12px, 400 weight, `#999999`, 1.3 line-height |
-| `chrome.byline` | 12px, 400 weight, `#999999`, 1.3 line-height |
-| `chrome.footer` | 12px, 400 weight, `#999999`, 1.3 line-height |
+The named presets are `essay`, `wire`, `broadsheet`, and `terminal`. Omit `theme` or pass `{}` for the default; `editorial` is removed. `clarity` is deprecated and contains only its background, axis, and eyebrow colors. `legacyEditorial` retains v8 typography for migration.
+
+Only authored theme overrides and derived values are stamped inline. Omitted CSS tokens remain overridable by host styles, including `--oc-border-radius`; removing an override restores the host's previous inline value. Export captures live token values.
---
@@ -1080,9 +1047,8 @@ Input for data table visualizations. Source: `core/src/types/spec.ts`.
| `striped` | `boolean` | `false` | Zebra striping on body rows. Hairlines only by default. |
| `sort` | `SortState` | first inline-bar column, descending | Initial sort applied before the user touches a header. `{ column: string, direction: 'asc' \| 'desc' }`. |
| `totalRow` | `boolean \| { label?: string }` | `false` | Sticky footer summing every quantitative column over the filtered rows (the whole result set, not just the current page). `true` uses the label `'Total'`. |
-| `compact` | `boolean` | `false` | **Deprecated.** Alias for `density: 'condensed'`; still honored, with a warning. |
| `responsive` | `boolean` | `true` | Whether the table adapts to container width. |
-| `watermark` | `boolean` | `true` | Whether to show the tryOpenData.ai watermark. Spec-level value takes precedence over mount/compile options. |
+| `watermark` | `boolean` | `true` | Whether to show the tryOpenData.ai watermark. Mount/compile options take precedence over spec and active breakpoint values. |
### ColumnConfig
@@ -1198,8 +1164,13 @@ Pass these through `MountOptions` (vanilla) or `ChartProps` (React).
| `onMarkClick` | `(event: MarkEvent) => void` | User clicks a data mark (bar, point, line, arc). |
| `onMarkHover` | `(event: MarkEvent) => void` | Mouse enters a data mark. |
| `onMarkLeave` | `() => void` | Mouse leaves a data mark. |
+| `onEdit` | `(transaction: EditTransaction) => void` | One committed authored edit; apply atomically with `applyEditTransaction` and retain its inverse for undo. |
| `onLegendToggle` | `(series: string, visible: boolean) => void` | User clicks a legend entry to show/hide a series. |
-| `onAnnotationClick` | `(annotation: Annotation, event: MouseEvent) => void` | User clicks an annotation element. |
+| `onAnnotationClick` | `(annotation: Annotation, event: MouseEvent, element: ElementRef) => void` | User clicks an annotation element. |
+| `onSelect` | `(element: ElementRef) => void` | An element is selected by click, Tab, or `select()`. Requires `interaction` `'select'` or `'edit'`. |
+| `onDeselect` | `(element: ElementRef) => void` | The selected element is deselected. |
+
+The `interaction` mount option (`'read' | 'select' | 'edit'`) gates selection and editing; when omitted, `onEdit` implies `'edit'` and `onSelect`/`onDeselect` imply `'select'`. Every `ElementRef` carries a JSON Pointer `path`. See [Editing and keyboard navigation](migrating-v9.md#editing-and-keyboard-navigation) for transaction persistence and cancelled-gesture behavior. See [Interaction mode](integration-guide.md#interaction-mode) for the full table.
### MarkEvent
@@ -1242,7 +1213,7 @@ Graphs render force-directed network visualizations on canvas. They support node
| `chrome` | `Chrome` | `undefined` | Editorial text. |
| `annotations` | `Annotation[]` | `undefined` | Annotations. |
| `theme` | `ThemeConfig` | `undefined` | Theme overrides. |
-| `watermark` | `boolean` | `true` | Whether to show the tryOpenData.ai watermark. Spec-level value takes precedence over mount/compile options. |
+| `watermark` | `boolean` | `true` | Whether to show the tryOpenData.ai watermark. Mount/compile options take precedence over spec and active breakpoint values. |
### GraphEncoding
@@ -1305,7 +1276,6 @@ Sankey diagrams show flows between stages. Each data row represents a link from
| `linkStyle` | `SankeyLinkColor` | `'gradient'`| Link coloring: `'gradient'`, `'source'`, `'target'`, `'neutral'`. |
| `linkOpacity` | `number` | `0.5` | Link fill opacity (0-1). Defaults to 0.75 in dark mode. |
| `nodeLabelAlign`| `string` | `'auto'` | Label placement: `'auto'`, `'left'`, `'right'`. |
-| `valueFormat` | `string` | `undefined` | d3-format string for values in tooltips and ARIA labels (e.g. `"$,.0f"`, `"~s"`). |
| `chrome` | `Chrome` | `undefined` | Editorial text (title, subtitle, source, byline, footer). |
| `legend` | `LegendConfig` | `undefined` | Legend display configuration. |
| `theme` | `ThemeConfig` | `undefined` | Theme overrides. |
@@ -1370,7 +1340,6 @@ Maps render TopoJSON geometries as SVG, join tabular data rows to features by id
| `theme` | `ThemeConfig` | `undefined` | Theme overrides. |
| `watermark` | `boolean` | `true` | Whether to show the tryOpenData.ai watermark. |
| `animation` | `AnimationSpec` | `undefined` | Entrance animation configuration. |
-| `valueFormat` | `string` | `undefined` | Deprecated: use `encoding.color.format` instead. d3-format string for color values in tooltips and the legend (e.g. `".1f"`). `encoding.color.format` wins when both are set. |
### GeoMapGeo
@@ -1509,7 +1478,7 @@ Every state renders as an equal-size square in a fixed 12x8 grid, so small state
| `theme` | `ThemeConfig` | `undefined` | Theme overrides. |
| `watermark` | `boolean` | `true` | Whether to show the tryOpenData.ai watermark. |
| `animation` | `AnimationSpec` | `undefined` | Entrance animation configuration. |
-| `valueFormat` | `string` | `undefined` | Deprecated: use `encoding.value.format` instead. d3-format string for tile values, legend labels, and tooltips. |
+| `valueFormat` | `string` | `undefined` | Tilemap fallback d3-format string for values, legend labels, and tooltips; `encoding.value.format` takes precedence. |
### TileMapEncoding
@@ -1524,7 +1493,7 @@ Every state renders as an equal-size square in a fixed 12x8 grid, so small state
- **Quantitative mode** renders a gradient legend bar with min/max labels below the grid. **Categorical mode** (string values, a `color` channel, or a `colors` map) renders swatch rows instead.
- States missing from the data render as empty tiles, keeping their place on the grid.
-- **Number formatting:** set `encoding.value.format` (d3-format string, e.g. `".1f"`, `"$,.0f"`, `"~s"`). It applies to tile values, legend labels, and tooltips, and wins over the deprecated top-level `valueFormat`. With record-map data and no explicit encoding, `valueFormat` still applies (with a deprecation warning).
+- **Number formatting:** set `encoding.value.format` (d3-format string, e.g. `".1f"`, `"$,.0f"`, `"~s"`). It applies to tile values, legend labels, and tooltips, and wins over top-level `valueFormat`. With record-map data and no explicit encoding, `valueFormat` remains supported.
- **`scale.scheme` fails validation on tilemap encoding channels.** The tilemap compile path never reads it; the top-level `palette` property is the mechanism tilemaps use for color.
### Tilemap example
@@ -1677,7 +1646,7 @@ const spec: ChartSpec = {
{ category: "Tablet", share: 7 },
],
encoding: {
- y: { field: "share", type: "quantitative" },
+ theta: { field: "share", type: "quantitative" },
color: { field: "category", type: "nominal" },
},
chrome: { title: "Traffic by device" },
@@ -1697,7 +1666,7 @@ const spec: ChartSpec = {
{ status: "Blocked", count: 5 },
],
encoding: {
- y: { field: "count", type: "quantitative" },
+ theta: { field: "count", type: "quantitative" },
color: { field: "status", type: "nominal" },
},
chrome: { title: "Task status" },
@@ -1837,9 +1806,9 @@ All builders accept field names as strings (auto-infer type from data) or full `
| `annotations` | `Annotation[]` | Annotations. |
| `responsive` | `boolean` | Responsive behavior. |
| `theme` | `ThemeConfig` | Theme overrides. |
-| `watermark` | `boolean` | Whether to show the tryOpenData.ai watermark. Spec-level value takes precedence over mount/compile options; defaults to `true`. |
+| `watermark` | `boolean` | Whether to show the tryOpenData.ai watermark. Mount/compile options take precedence over the spec; defaults to `true`. |
-`FieldRef` is `string | EncodingChannel`. When a string is provided, `inferFieldType()` samples up to 20 data values to determine the encoding type (quantitative, temporal, or nominal).
+`FieldRef` is `string | EncodingChannel`. When a string is provided, `inferFieldType()` uses the shared builder/compiler inference: sample up to 50 rows, ignore nulls, and require more than 80% agreement. Numeric strings are considered before dates, so a column of `'2020'` values is quantitative. Set the channel type explicitly when years should be ordinal or temporal.
### Builder example
@@ -1870,12 +1839,12 @@ const result = validateSpec(spec);
### ValidationError
-| Field | Type | Description |
-| ------------ | --------------------- | ------------------------------------------------------------- |
-| `message` | `string` | Human-readable error description. |
-| `path` | `string \| undefined` | Dot-path to the field that failed, e.g. `'encoding.x.field'`. |
-| `code` | `ValidationErrorCode` | Machine-readable error code. |
-| `suggestion` | `string` | Actionable fix suggestion. |
+| Field | Type | Description |
+| ------------ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
+| `message` | `string` | Human-readable error description. |
+| `path` | `string` | Root-relative RFC 6901 JSON Pointer to the value that failed, e.g. `'/encoding/x/field'` or `'/layer/1/data/0/value'`. The root is `''`. |
+| `code` | `ValidationErrorCode` | Machine-readable error code. |
+| `suggestion` | `string` | Actionable fix suggestion. |
### ValidationErrorCode
@@ -1901,14 +1870,14 @@ const result = validateSpec(spec);
## Authoring foundation additions
-The separate [OpenStudio application](https://github.com/tryopendata/openstudio) consumes these additive interfaces. Existing v8 visual and animation defaults remain unchanged.
+These library interfaces support authoring and reader applications. OpenStudio adoption is separate; see [Migrating to v9](migrating-v9.md) for the changed defaults and removed APIs.
-- **Theme:** import `clarity` for white/ink surfaces, Inter titles and 12px axes; `legacyEditorial` restores documented v8 typography without freezing layout behavior.
+- **Theme:** v9 defaults include 12px axes/small text and larger chrome gaps. `clarity` is deprecated and retains only background, axis, and eyebrow colors. `legacyEditorial` restores v8 typography; `migrateSpecV8` can materialize the wider v8 styling contract without freezing layout behavior.
- **Motion:** `animation: 'fluid' | 'subtle' (omit the preset for v8 timing)`; explicit phases override their preset. A phase can use `spring: { stiffness: 320, damping: 36, mass: 1 }` or the existing duration/easing configuration. Combining a spring with duration/easing in the same phase is an error. Quantitative springs are bounded, with a finite terminal state. Omitted/false animation remains disabled.
- **Responsiveness:** `responsive: { preserveEnd: true, labelSummary: true }` retains the final x-axis boundary and moves overflowing endpoint identities to a wrapped list. Explicit axis values and author choices remain authoritative. `chromeLayout: 'grow'` gives that content space. Tables may opt into `mobileLayout: 'priority'`; `cards` remains the default, with `scroll` also available.
- **Field metadata:** chart, table and map documents accept `fields`, keyed by field name. Each entry may declare `type` (`text`, `number`, `date`), `role: 'identity'`, `label`, `unit`, `measure`, `provenance`, `representation` (`number`, `percent`, `proportion`), `interval: { lower: 'lowerField', upper: 'upperField' }`, or `statusField`. Supplied intervals are never synthesized. Percent means 0–100 and proportion means 0–1.
- **Reader identity:** a unique authored `encoding.key` supports observation persistence. Missing/duplicate keys clear observation pins on data replacement. Aggregated observations are groups, not raw rows, and the reference reader clears them on data/grouping revision. The `createReader` controller is independent from editor selection and uses `ChartInstance.sampleLayout()` for supported moving geometry. Call `reader.update(spec)` after `chart.update(spec)`; it reconciles visibility against the compiled destination marks even when the source data array is unchanged. An explicit `visibleKeys` override remains supported. Layered charts retain per-layer hover/crosshair inspection, but reader pinning across layers is not yet supported: a `LayerSpec` clears pins with an announcement rather than guessing cross-layer identity.
-- **Design review:** `diagnoseChart(spec, layout)` returns structured checks with code, severity, checkability, path, viewport, explanation and origin. `CompileOptions.onDiagnostic` receives these checks. `recommendCompositions(data, question, roles)` requires explicit field roles and returns explained alternatives. `applySpecPatch` rejects stale snapshot preconditions; hosts should preview patches and retain undo. The reference editor uses the full authored document for patches even while previewing a filtered scene.
+- **Design review:** `diagnoseChart(spec, layout)` returns structured checks with code, severity, checkability, path, viewport, explanation and origin. `CompileOptions.onDiagnostic` receives these checks. `recommendCompositions(data, question, roles)` requires explicit field roles and returns explained alternatives. A diagnostic `fix` is a `SpecPatch` whose `changes` are `EditOp`s. `applySpecPatch(spec, patch)` rejects stale snapshot preconditions, applies the operations atomically through `applyEditTransaction`, and returns `{ spec, inverse }`; apply `inverse` to the patched document to undo. Hosts should preview patches before applying them. The reference editor uses the full authored document for patches even while previewing a filtered scene.
Host-neutral identity, comparison and explicit dataset-binding helpers are also exported from the engine. They provide foundations for future linked views, without implicitly declaring compatible measures or raw-row/aggregate relationships. See the [delivery record](design-evolution.md) for the advanced capabilities and release gates still pending.
@@ -1921,15 +1890,15 @@ Maps, tile maps, bar lists and Sankey support arrows for inspection and Enter/Sp
Table `rowKey` values must be unique to support identity across revisions. Numeric `1` and string `"1"` are distinct. Missing/duplicate keys emit `TABLE_ROW_KEY_AMBIGUOUS` and use revision-local identities. Treat compiled row IDs as opaque. CSV export includes all filtered and sorted rows, not just the current pagination page.
-### Compact callout limits and note sizing (8.9.1)
+### Compact callout limits and note sizing
`responsive.compactAnnotationLimit?: number | null` is an optional nonnegative
-integer limit below 400px outer width. Omitted/null means unlimited; zero demotes
-all eligible callouts. `responsive: false` callouts are exempt. Priority/source
+integer limit below 400px outer width. Omitted means one; null means unlimited,
+and zero demotes all eligible callouts. `responsive: false` callouts are exempt. Priority/source
order precedes coverage/collision thinning. Existing disabled-response and
short-height rules still apply. Facet panel width does not trigger this limit.
Read `layout.chrome.noteBandHeight` for note space and `layout.dimensions.height`
for the final rendered/exported height. `chromeLayout: 'grow'` also supports facets;
`chrome.grownHeight` reports engine-added height for auto-height consumers.
-See [annotation migration guidance](./migrating-v8.md#annotation-layout-opt-ins).
+See [annotation migration guidance](./migrating-v9.md#changed-annotation-and-figure-defaults).
diff --git a/docs/tables.md b/docs/tables.md
index e3bd2433..d82ab10e 100644
--- a/docs/tables.md
+++ b/docs/tables.md
@@ -185,7 +185,6 @@ All features are opt-in via top-level `TableSpec` properties.
| Striping | `striped: true` | `false` | Zebra rows. Hairlines only by default |
| Initial sort | `sort: { column, direction }` | first inline-bar column, desc | Sort applied before the user touches a header |
| Total row | `totalRow: true` | `false` | Sticky footer summing every quantitative column with a summable cell type (text/bar/heatmap). Set `columns[].total: false` to leave an individual column's total cell blank |
-| Compact mode | `compact: true` | `false` | **Deprecated.** Alias for `density: 'condensed'`; warns |
| Responsive | `responsive: true` | `true` | Table adapts to container width |
In controlled mode a `sort` of `null` means "the user cleared sorting" and
@@ -232,6 +231,15 @@ share whatever space is left. If the explicit widths on their own would
exceed the container, they're scaled down proportionally (floored at 60px)
and the engine emits a `TABLE_WIDTH_OVERFLOW` warning through `onWarn`.
+### Surface
+
+Tables paint `theme.colors.background`, which is transparent by default, so a
+table sits on the host surface like a chart does. The header, totals row,
+search input and pager buttons are transparent too; the opt-in sticky first
+column keeps the mode's `--oc-bg` so scrolled cells don't show through. Inline
+bar fills render at 40% opacity in dark mode (18% in light). Set
+`theme.colors.background` for an opaque table.
+
### Full-featured example
```ts
diff --git a/docs/theming-tokens.md b/docs/theming-tokens.md
index 25538cdc..1efc1a42 100644
--- a/docs/theming-tokens.md
+++ b/docs/theming-tokens.md
@@ -1,16 +1,12 @@
-# Migration Guide
+# Theming Design Tokens
-## Theming Design Tokens
+`ThemeConfig` controls colors, typography, spacing, rules, and component surfaces.
+Defaults are shared by the JavaScript theme and generated CSS tokens. v9
+consolidates authored deep merging and leaves omitted CSS tokens available to
+host styles. For removed forms and legacy styling, see [Migrating to v9](migrating-v9.md).
-This release widens `ThemeConfig` to expose every design-decision field and unifies CSS custom properties with the JS theme engine. All changes are backward-compatible. Existing theme configs continue to work without modification.
+### ThemeConfig fields
-### What changed
-
-**Before:** `ThemeConfig` covered colors, font family/sizes, 4 spacing fields, border radius, and chrome color (string only). Font weights, chrome typography, semantic colors, and additional spacing fields were hardcoded in `DEFAULT_THEME` with no override path. CSS custom properties in `tokens.css`/`dark.css` were hand-maintained separately from the JS theme.
-
-**After:** `ThemeConfig` covers everything. CSS custom properties are generated from the resolved JS theme at mount time. The static CSS files serve as SSR/pre-mount fallback defaults.
-
-### New ThemeConfig fields
```ts
const theme: ThemeConfig = {
@@ -52,8 +48,8 @@ const theme: ThemeConfig = {
subtitle: { fontWeight: 400, lineHeight: 1.4 },
eyebrow: { color: '#e3120b', fontWeight: 700 },
- // Plain strings still work (color-only, backward-compatible)
- source: '#666',
+ // Color-only overrides use the same object form.
+ source: { color: '#666' },
},
// New: series color assignment strategy
@@ -93,11 +89,10 @@ The neutral grays are surface-aware: on light backgrounds the second series gets
### Named presets
-Five presets exported from `@opendata-ai/openchart-core`:
+Four primary presets exported from `@opendata-ai/openchart-core`:
| Preset | Feel |
|--------|------|
-| `editorial` | Current default (empty config, zero drift) |
| `essay` | Serif titles, warm background, generous spacing |
| `wire` | Monospace, dense, tight chrome, dashboard feel |
| `broadsheet` | Newspaper editorial: warm paper, red masthead `rule` and eyebrow, 24/700 title, square corners |
@@ -147,11 +142,11 @@ fixed zinc ladder. A warm or cool custom theme gets warm or cool grays for
free instead of zinc showing up uninvited beside the palette. The ramp lives
on `ResolvedTheme.colors.neutral` (`100`/`200`/`300`/`400`/`600`/`800`, plus
`secondary` = `800`, `faint` = `300`, `border` = `100`, plus `surface`) and is
-computed once in `resolveTheme`/`adaptTheme`; mounts stamp the CSS tokens from
-it. `surface` is the opaque color the theme paints on — the background itself
+computed once in `resolveTheme`/`adaptTheme`. Mounts stamp derived tokens when
+the relevant theme colors are authored; omitted tokens use the CSS cascade. `surface` is the opaque color the theme paints on — the background itself
when it is opaque, otherwise the mode's `--oc-bg` token — and is the single
source for knockout rings, stacked-segment seams, table cell fills, graph node
-rings and the stamped `--oc-bg`. When the
+rings, annotation marker/plate fills, and authored background tokens. When the
background is transparent (the default) the ramp falls back to the static
token defaults, which is why the default theme and the generated `tokens.css`
never disagree.
@@ -169,9 +164,9 @@ never disagree.
| `--oc-map-hover-dim` | 0.75 | map-local override; dimming 3000 county paths at 0.3 reads as a blackout, so maps use a lighter dim and only on legend-bin hover, never on feature hover |
| `--oc-positive-tint` / `--oc-negative-tint` | 10% color-mix toward `--oc-bg` | delta chip backgrounds |
-`--oc-border-radius` (8px) drives containers and tooltips. In v8, mounts stamp
-resolved theme tokens inline, so ordinary stylesheet overrides of these tokens
-do not win. Set `theme.borderRadius` to change this value. Mark geometry uses the fixed `--oc-radius-sm`
+`--oc-border-radius` (8px) drives containers and tooltips. In v9, CSS overrides
+work when `theme.borderRadius` is omitted. An authored `theme.borderRadius`
+takes precedence. Mark geometry uses the fixed `--oc-radius-sm`
regardless, so a theme with `borderRadius: 0` still draws a 2px bar-end
radius.
@@ -180,7 +175,7 @@ radius.
`token-theme-parity` test asserts this, along with the four weight tokens,
`--oc-gridline`, `--oc-axis`, `--oc-border-radius`, `colors.neutral`, and
`--oc-animation-stagger`, so the JS theme and the generated CSS tokens can't
-drift apart the way they did before this release.
+drift apart.
### The editorial rule
@@ -200,16 +195,32 @@ as `chrome.*.color` and needs no dark-mode branch. `computeChrome` reserves
### CSS custom properties
-`--oc-*` custom properties are now stamped on each `.oc-root` container from the resolved JS theme at mount time. This means:
-
-- **Two charts with different themes on the same page** each get correct `--oc-*` values (per-container scoping).
-- **`tokens.css` and `dark.css` are fallback defaults**, not the source of truth. They still work for SSR, static HTML, and pre-mount styling.
-- **If you were reading `--oc-*` values from CSS** to style surrounding UI, those values now reflect the actual resolved theme rather than the hardcoded defaults. This is almost certainly what you wanted, but worth noting.
-
-No action required. If you import `tokens.css`/`dark.css`, keep doing so. The JS-stamped properties take precedence via inline style specificity.
-
-
-### Annotation tokens (8.9.1)
+Defaults remain in `tokens.css` and `dark.css`. The shared renderer helper stamps
+only authored theme leaves and values derived from them. A host stylesheet can
+override omitted tokens, including `--oc-border-radius`; an authored theme value
+wins via its inline style. Removing a theme override restores the host value.
+SVG and raster exports retain the live CSS token values before leaving the page.
+
+Theme layers deep-merge in this order: spec theme, provider theme, component prop
+(or mount/compile option). For layered charts, chrome, axes and legend use the
+layer theme (or the first leaf's when the layer sets none), and each leaf's marks
+use its own theme merged over the layer theme.
+Objects merge by leaf; arrays and `{ light, dark }` pairs replace atomically.
+`colors: string[]` remains shorthand for `colors.categorical`. `null` replaces:
+use `rule: null` to remove a preset rule. Partial rules inherit unspecified
+preset values; without a preset, width is 40px, thickness is 3px, and color is
+theme text ink. Authored `chrome..fontSize` overrides the general
+`fonts.sizes` scale. Chrome color shorthand is now `{ color: '...' }`.
+
+The default small/axis tick/source/byline/footer size is 12px; chrome-to-chart
+spacing is 16px and chart-to-footer spacing is 12px. The background remains
+transparent. The empty `editorial` preset was removed (omit `theme` instead).
+`clarity` is deprecated and retains only its background, axis, and eyebrow
+colors. Use `ThemeConfig` for input and `ResolvedTheme` for output; the `Theme`
+alias was removed.
+
+
+### Annotation tokens
`theme.colors` accepts `annotationLabel`, `annotationMuted`, `annotationLeader`,
`annotationRule`, `annotationRangeFill`, `annotationMarkerFill`, and
@@ -218,4 +229,6 @@ are `--oc-annotation-label`, `--oc-annotation-muted`, `--oc-annotation-leader`,
`--oc-annotation-rule`, `--oc-annotation-range-fill`, `--oc-annotation-marker-fill`,
and `--oc-annotation-plate`. Explicit annotation colors override the tokens.
Arrow connectors match label ink. Existing `annotationText`/`annotationFill`
-uses are unchanged. Omitted annotation tokens preserve current 8.x output.
+uses are unchanged. Default rule ink is #888888 in light mode and #9ca3af in
+dark mode; range fill is #f0c040. Marker and plate fills follow the resolved
+neutral surface unless explicitly authored.
diff --git a/e2e/annotation-invariants/curves.spec.ts b/e2e/annotation-invariants/curves.spec.ts
index 55359742..26edbb6d 100644
--- a/e2e/annotation-invariants/curves.spec.ts
+++ b/e2e/annotation-invariants/curves.spec.ts
@@ -13,6 +13,9 @@ for (const width of [320, 390, 768, 1280])
const charts = page.getByTestId('annotation-curve');
await expect(charts).toHaveCount(10);
for (const chart of await charts.all()) {
+ await expect(chart).toHaveAttribute('data-oc-fonts-state', 'ready');
+ if (colorScheme === 'dark') await expect(chart).toHaveClass(/oc-dark/);
+ else await expect(chart).not.toHaveClass(/oc-dark/);
await expect(chart.locator('.oc-annotation-label')).toBeVisible();
await chart.dispatchEvent('fixture-layout');
const layout: ChartLayout = JSON.parse((await chart.getAttribute('data-layout'))!);
diff --git a/e2e/annotation-invariants/gestures.spec.ts b/e2e/annotation-invariants/gestures.spec.ts
index 6330d851..190654ac 100644
--- a/e2e/annotation-invariants/gestures.spec.ts
+++ b/e2e/annotation-invariants/gestures.spec.ts
@@ -1,4 +1,5 @@
import { expect, test } from '@playwright/test';
+import { waitForResizeSettled } from '../settle';
const story = '/?story=features--annotation-gestures&mode=preview';
for (const width of [320, 390, 768, 1280])
@@ -115,7 +116,9 @@ for (const cancellation of ['Escape', 'pointercancel', 'touchcancel', 'replaceme
else if (cancellation === 'width') {
await page.setViewportSize({ width: 700, height: 900 });
await expect.poll(() => host.locator('svg').getAttribute('viewBox')).not.toContain('768');
- await page.waitForTimeout(100);
+ // Release only after every debounced resize render has landed, so the
+ // pointerup meets the rebuilt chart rather than a pending re-render.
+ await waitForResizeSettled(host);
} else await page.evaluate((type) => document.dispatchEvent(new Event(type)), cancellation);
await page.mouse.up();
await expect(page.getByTestId('edit-count')).toHaveText('0');
diff --git a/e2e/annotation-invariants/layout.spec.ts b/e2e/annotation-invariants/layout.spec.ts
index cfb023c9..e7d509b8 100644
--- a/e2e/annotation-invariants/layout.spec.ts
+++ b/e2e/annotation-invariants/layout.spec.ts
@@ -1,99 +1,158 @@
import type { ChartLayout, ResolvedAnnotation } from '@opendata-ai/openchart-core';
-import { expect, test } from '@playwright/test';
+import { expect, test, type Page } from '@playwright/test';
+import { waitForResizeSettled } from '../settle';
-for (const width of [320, 390, 768, 1280]) for (const colorScheme of ['light', 'dark'] as const) {
- for (const story of ['reference-labels', 'crowded-callouts', 'notes-sizing']) {
- test(`${width} ${colorScheme}: ${story} layout and export`, async ({ page }) => {
- await page.setViewportSize({ width, height: 900 });
- await page.emulateMedia({ colorScheme });
- await page.goto(`/?story=features--annotation-${story}&mode=preview`);
- const charts = page.getByTestId('annotation-layout');
- await expect(charts.first().locator('svg')).toBeVisible();
- await page.evaluate(() => document.fonts.ready);
- for (const chart of await charts.all()) {
- await chart.dispatchEvent('fixture-layout');
- const layout: ChartLayout = JSON.parse((await chart.getAttribute('data-layout'))!);
- expect(layout.theme.isDark).toBe(colorScheme === 'dark');
- const groups = layout.facet?.panels.map(p => p.annotations) ?? [layout.annotations];
- for (const annotations of groups) {
- const visible = annotations.filter(a => a.label?.visible && !a.footnoteIndex);
- for (const annotation of visible) {
- const box = annotation.bounds ?? annotation.label!.bounds;
- if (!box) continue;
- expect(box.x, annotation.id).toBeGreaterThanOrEqual(-1);
- expect(box.y, annotation.id).toBeGreaterThanOrEqual(-1);
- expect(box.x + box.width, annotation.id).toBeLessThanOrEqual(layout.dimensions.width + 1);
- expect(box.y + box.height, annotation.id).toBeLessThanOrEqual(layout.dimensions.height + 1);
+for (const width of [320, 390, 768, 1280])
+ for (const colorScheme of ['light', 'dark'] as const) {
+ for (const story of ['reference-labels', 'crowded-callouts', 'notes-sizing']) {
+ test(`${width} ${colorScheme}: ${story} layout and export`, async ({ page }) => {
+ await page.setViewportSize({ width, height: 900 });
+ await page.emulateMedia({ colorScheme });
+ await page.goto(`/?story=features--annotation-${story}&mode=preview`);
+ const charts = page.getByTestId('annotation-layout');
+ await expect(charts.first().locator('svg')).toBeVisible();
+ await page.evaluate(() => document.fonts.ready);
+ for (const chart of await charts.all()) {
+ await expect(chart).toHaveAttribute('data-oc-fonts-state', 'ready');
+ if (colorScheme === 'dark') await expect(chart).toHaveClass(/oc-dark/);
+ else await expect(chart).not.toHaveClass(/oc-dark/);
+ await chart.dispatchEvent('fixture-layout');
+ const layout: ChartLayout = JSON.parse((await chart.getAttribute('data-layout'))!);
+ expect(layout.theme.isDark).toBe(colorScheme === 'dark');
+ const groups = layout.facet?.panels.map((p) => p.annotations) ?? [layout.annotations];
+ for (const annotations of groups) {
+ const visible = annotations.filter((a) => a.label?.visible && !a.footnoteIndex);
+ for (const annotation of visible) {
+ const box = annotation.bounds ?? annotation.label!.bounds;
+ if (!box) continue;
+ expect(box.x, annotation.id).toBeGreaterThanOrEqual(-1);
+ expect(box.y, annotation.id).toBeGreaterThanOrEqual(-1);
+ expect(box.x + box.width, annotation.id).toBeLessThanOrEqual(
+ layout.dimensions.width + 1,
+ );
+ expect(box.y + box.height, annotation.id).toBeLessThanOrEqual(
+ layout.dimensions.height + 1,
+ );
+ }
+ const unpinned = visible.filter((a) => !a.id?.match(/^crowded-1[23]$/));
+ for (let i = 0; i < unpinned.length; i++)
+ for (let j = i + 1; j < unpinned.length; j++) {
+ const a = unpinned[i].bounds,
+ b = unpinned[j].bounds;
+ if (!a || !b) continue;
+ const overlap =
+ Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x) > 1 &&
+ Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y) > 1;
+ expect(overlap, `${unpinned[i].id}/${unpinned[j].id}`).toBe(false);
+ }
+ if (story === 'crowded-callouts') {
+ expect(visible.filter((a) => a.id?.match(/^crowded-1[23]$/))).toHaveLength(2);
+ expect(visible.length + (layout.chrome.footnotes?.length ?? 0)).toBe(14);
+ }
+ if (story === 'notes-sizing')
+ expect(visible.length + (layout.chrome.footnotes?.length ?? 0)).toBe(2);
+ for (const a of visible) assertConnector(a);
}
- const unpinned = visible.filter(a => !a.id?.match(/^crowded-1[23]$/));
- for (let i = 0; i < unpinned.length; i++) for (let j = i + 1; j < unpinned.length; j++) {
- const a = unpinned[i].bounds, b = unpinned[j].bounds;
- if (!a || !b) continue;
- const overlap = Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x) > 1 && Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y) > 1;
- expect(overlap, `${unpinned[i].id}/${unpinned[j].id}`).toBe(false);
- }
- if (story === 'crowded-callouts') {
- expect(visible.filter(a => a.id?.match(/^crowded-1[23]$/))).toHaveLength(2);
- expect(visible.length + (layout.chrome.footnotes?.length ?? 0)).toBe(14);
- }
- if (story === 'notes-sizing') expect(visible.length + (layout.chrome.footnotes?.length ?? 0)).toBe(2);
- for (const a of visible) assertConnector(a);
+ expect(
+ await chart
+ .locator('svg')
+ .first()
+ .evaluate((svg) => svg.getBoundingClientRect().height),
+ ).toBeCloseTo(layout.dimensions.height, 1);
+ await chart.dispatchEvent('fixture-export');
+ const exported = (await chart.getAttribute('data-export'))!;
+ expect(exported).not.toMatch(/oc-edit-handle|oc-selection-handle/);
+ await assertExportNotes(page, exported, layout);
}
- expect(await chart.locator('svg').first().evaluate(svg => svg.getBoundingClientRect().height)).toBeCloseTo(layout.dimensions.height, 1);
- await chart.dispatchEvent('fixture-export');
- const exported = (await chart.getAttribute('data-export'))!;
- expect(exported).not.toMatch(/oc-edit-handle|oc-selection-handle/);
- for (const note of layout.chrome.footnotes ?? []) {
- // Plain-text notes remain intentional in Track A, including literal bold markers.
- expect(exported).toContain((note.lines?.[0] ?? `${note.index}. ${note.text}`).replaceAll('&', '&').replaceAll('<', '<'));
+ if (story === 'notes-sizing') {
+ const canvas = page.getByTestId('annotation-canvas');
+ await expect(canvas.locator('canvas.oc-mark-canvas')).toBeAttached();
+ await canvas.dispatchEvent('fixture-layout');
+ await canvas.dispatchEvent('fixture-export');
+ const canvasLayout: ChartLayout = JSON.parse((await canvas.getAttribute('data-layout'))!);
+ await assertExportNotes(page, (await canvas.getAttribute('data-export'))!, canvasLayout);
+ // Canvas changes only the mark renderer; annotation geometry and the
+ // figure-owned note band must match the equivalent SVG fixture.
+ const svgLayout: ChartLayout = JSON.parse(
+ (await charts.nth(4).getAttribute('data-layout'))!,
+ );
+ expect(canvasLayout.annotations).toEqual(svgLayout.annotations);
+ expect(canvasLayout.chrome.footnotes).toEqual(svgLayout.chrome.footnotes);
+ expect(canvasLayout.dimensions).toEqual(svgLayout.dimensions);
+ await canvas.dispatchEvent('fixture-png');
+ await expect(canvas).toHaveAttribute('data-png', /width/);
+ const png = JSON.parse((await canvas.getAttribute('data-png'))!);
+ expect(png.width).toBe(Math.floor(canvasLayout.dimensions.width));
+ expect(png.height).toBe(Math.floor(canvasLayout.dimensions.height));
+ const auto = page.getByTestId('annotation-auto-height');
+ await auto.dispatchEvent('fixture-layout');
+ const first: ChartLayout = JSON.parse((await auto.getAttribute('data-layout'))!);
+ expect(first.dimensions.height).toBeCloseTo(400 + (first.chrome.grownHeight ?? 0), 1);
+ await auto.dispatchEvent('fixture-png');
+ await expect(auto).toHaveAttribute('data-png', /width/);
+ expect(JSON.parse((await auto.getAttribute('data-png'))!).height).toBe(
+ Math.floor(first.dimensions.height),
+ );
+ // Auto-height grows the host, which re-fires the ResizeObserver. Wait
+ // for the mount to handle that echo (and any echo of its own render);
+ // a self-resize loop never settles, and a single re-render would show
+ // up as a changed height below.
+ await waitForResizeSettled(auto);
+ await auto.dispatchEvent('fixture-layout');
+ expect(JSON.parse((await auto.getAttribute('data-layout'))!).dimensions.height).toBe(
+ first.dimensions.height,
+ );
}
- }
- if (story === 'notes-sizing') {
- const canvas = page.getByTestId('annotation-canvas');
- await expect(canvas.locator('canvas.oc-mark-canvas')).toBeAttached();
- await canvas.dispatchEvent('fixture-layout');
- await canvas.dispatchEvent('fixture-export');
- const canvasLayout: ChartLayout = JSON.parse((await canvas.getAttribute('data-layout'))!);
- for (const note of canvasLayout.chrome.footnotes ?? []) expect((await canvas.getAttribute('data-export'))!).toContain(note.text.replaceAll('&', '&').replaceAll('<', '<').split(' ').slice(0, 2).join(' '));
- // Canvas changes only the mark renderer; annotation geometry and the
- // figure-owned note band must match the equivalent SVG fixture.
- const svgLayout: ChartLayout = JSON.parse((await charts.nth(4).getAttribute('data-layout'))!);
- expect(canvasLayout.annotations).toEqual(svgLayout.annotations);
- expect(canvasLayout.chrome.footnotes).toEqual(svgLayout.chrome.footnotes);
- expect(canvasLayout.dimensions).toEqual(svgLayout.dimensions);
- await canvas.dispatchEvent('fixture-png');
- await expect(canvas).toHaveAttribute('data-png', /width/);
- const png = JSON.parse((await canvas.getAttribute('data-png'))!);
- expect(png.width).toBe(Math.floor(canvasLayout.dimensions.width));
- expect(png.height).toBe(Math.floor(canvasLayout.dimensions.height));
- const auto = page.getByTestId('annotation-auto-height');
- await auto.dispatchEvent('fixture-layout');
- const first: ChartLayout = JSON.parse((await auto.getAttribute('data-layout'))!);
- expect(first.dimensions.height).toBeCloseTo(400 + (first.chrome.grownHeight ?? 0), 1);
- await auto.dispatchEvent('fixture-png');
- await expect(auto).toHaveAttribute('data-png', /width/);
- expect(JSON.parse((await auto.getAttribute('data-png'))!).height).toBe(Math.floor(first.dimensions.height));
- await page.waitForTimeout(250);
- await auto.dispatchEvent('fixture-layout');
- expect(JSON.parse((await auto.getAttribute('data-layout'))!).dimensions.height).toBe(first.dimensions.height);
- }
- });
+ });
+ }
}
-}
function assertConnector(annotation: ResolvedAnnotation) {
const connector = annotation.label?.connector;
const bounds = annotation.bounds;
if (!connector?.endpoint || !bounds || connector.style === 'drop-line') return;
- const cx = bounds.x + bounds.width / 2, cy = bounds.y + bounds.height / 2;
- const dx = connector.endpoint.x - cx, dy = connector.endpoint.y - cy;
+ const cx = bounds.x + bounds.width / 2,
+ cy = bounds.y + bounds.height / 2;
+ const dx = connector.endpoint.x - cx,
+ dy = connector.endpoint.y - cy;
const t = Math.min((bounds.width / 2 + 2) / Math.abs(dx), (bounds.height / 2 + 2) / Math.abs(dy));
if (connector.labelAttachment === 'left-center') {
expect(connector.from.x).toBeCloseTo(bounds.x - 8, 1);
expect(connector.from.y).toBeCloseTo(cy, 1);
} else {
- expect(Math.hypot(connector.from.x - (cx + dx * t), connector.from.y - (cy + dy * t))).toBeCloseTo(6, 1);
+ expect(
+ Math.hypot(connector.from.x - (cx + dx * t), connector.from.y - (cy + dy * t)),
+ ).toBeCloseTo(6, 1);
}
const radius = annotation.dot ? annotation.dot.radius + annotation.dot.strokeWidth / 2 : 0;
- expect(Math.hypot(connector.to.x - connector.endpoint.x, connector.to.y - connector.endpoint.y)).toBeCloseTo(connector.arrow ? Math.max(radius + 2, 2) : Math.max(radius + 3, 4), 1);
+ expect(
+ Math.hypot(connector.to.x - connector.endpoint.x, connector.to.y - connector.endpoint.y),
+ ).toBeCloseTo(connector.arrow ? Math.max(radius + 2, 2) : Math.max(radius + 3, 4), 1);
+}
+
+async function assertExportNotes(page: Page, source: string, layout: ChartLayout) {
+ const notes = await page.evaluate((svg) => {
+ const doc = new DOMParser().parseFromString(svg, 'image/svg+xml');
+ return {
+ controls: doc.querySelectorAll(
+ '.oc-note-active, .oc-selection-overlay, .oc-anchor-handle, .oc-connector-handle',
+ ).length,
+ notes: [...doc.querySelectorAll('[data-note-spec-index]')].map((note) => ({
+ label: note.getAttribute('aria-label'),
+ lines: [...note.children].map((line) => line.textContent),
+ bold: [...note.querySelectorAll('[font-weight="700"]')].map((span) => span.textContent),
+ })),
+ };
+ }, source);
+ expect(notes.controls).toBe(0);
+ expect(notes.notes).toHaveLength(layout.chrome.footnotes?.length ?? 0);
+ for (const [index, note] of (layout.chrome.footnotes ?? []).entries()) {
+ const clean = (text: string) => text.replaceAll('**', '');
+ expect(notes.notes[index].label).toBe(
+ `${note.index}. ${clean(note.text)}${note.subtitle ? `. ${clean(note.subtitle)}` : ''}`,
+ );
+ expect(notes.notes[index].lines).toEqual((note.lines ?? []).map(clean));
+ if (note.text.includes('**') || note.subtitle?.includes('**'))
+ expect(notes.notes[index].bold.length).toBeGreaterThan(0);
+ }
}
diff --git a/e2e/annotation-invariants/notes.spec.ts b/e2e/annotation-invariants/notes.spec.ts
new file mode 100644
index 00000000..a419c97e
--- /dev/null
+++ b/e2e/annotation-invariants/notes.spec.ts
@@ -0,0 +1,74 @@
+import { expect, test } from '@playwright/test';
+import { waitForResizeSettled } from '../settle';
+
+for (const width of [320, 390, 768, 1280])
+ for (const colorScheme of ['light', 'dark'] as const) {
+ test(`${width} ${colorScheme}: notes and markers share keyboard focus and clean export`, async ({
+ page,
+ }) => {
+ await page.setViewportSize({ width, height: 900 });
+ await page.emulateMedia({ colorScheme });
+ await page.goto('/?story=features--annotation-crowded-callouts&mode=preview');
+ const chart = page.getByTestId('annotation-layout').first();
+ const firstNote = chart.locator('[data-note-spec-index]').first();
+ await expect(chart.locator('svg')).toBeVisible();
+ await page.evaluate(() => document.fonts.ready);
+ await expect(chart).toHaveAttribute('data-oc-fonts-state', 'ready');
+ if (colorScheme === 'dark') await expect(chart).toHaveClass(/oc-dark/);
+ else await expect(chart).not.toHaveClass(/oc-dark/);
+ // This interaction fixture needs notes even when the viewport can fit all
+ // callouts. Full-width geometry remains covered by layout.spec.ts.
+ if (width === 1280) {
+ const chartWidth = await chart.evaluate((host) => {
+ host.style.maxWidth = '390px';
+ return host.clientWidth;
+ });
+ await expect
+ .poll(async () =>
+ chart.locator('svg').evaluate((svg) => (svg as SVGSVGElement).viewBox.baseVal.width),
+ )
+ .toBe(chartWidth);
+ }
+ // A re-render replaces the focused note, so let the entrance animation
+ // (which defers resizes) finish and the debounced resize settle before
+ // testing a user's focus action.
+ await expect(chart.locator('svg')).not.toHaveClass(/oc-animate/);
+ await waitForResizeSettled(chart);
+ await expect(firstNote).toBeVisible();
+ const source = await firstNote.getAttribute('data-note-spec-index');
+ const note = chart.locator(`[data-note-spec-index="${source}"]`);
+ const marker = chart.locator(`[data-note-marker-index="${source}"]`).first();
+ await expect(chart.locator('svg')).toHaveAttribute('role', 'group');
+ await expect(note).toHaveAttribute('role', 'listitem');
+ await expect(marker).toHaveAttribute('role', 'button');
+ await expect(note).not.toContainText('**');
+ await note.focus();
+ await expect(note).toHaveClass(/oc-note-active/);
+ await expect(marker).toHaveClass(/oc-note-active/);
+ await note.press('Enter');
+ await expect(marker).toBeFocused();
+ await marker.press('Space');
+ await expect(note).toBeFocused();
+ await expect(chart.locator('textarea, input, [contenteditable]')).toHaveCount(0);
+ await chart.dispatchEvent('fixture-export');
+ const result = await chart.evaluate((host) => {
+ const doc = new DOMParser().parseFromString(
+ (host as HTMLElement).dataset.export!,
+ 'image/svg+xml',
+ );
+ return {
+ active: doc.querySelectorAll(
+ '.oc-note-active, .oc-anchor-handle, .oc-connector-handle, .oc-selection-overlay',
+ ).length,
+ notes: [...doc.querySelectorAll('[data-note-spec-index]')].map((note) =>
+ note.getAttribute('aria-label'),
+ ),
+ };
+ });
+ expect(result.active).toBe(0);
+ expect(result.notes).toContain(await note.getAttribute('aria-label'));
+ await expect(note).toHaveClass(/oc-note-active/);
+ await note.press('Escape');
+ await expect(chart.locator('.oc-note-active')).toHaveCount(0);
+ });
+ }
diff --git a/e2e/chart-map-design/attribution.spec.ts b/e2e/chart-map-design/attribution.spec.ts
new file mode 100644
index 00000000..600b225d
--- /dev/null
+++ b/e2e/chart-map-design/attribution.spec.ts
@@ -0,0 +1,41 @@
+import { expect, test } from '@playwright/test';
+
+for (const width of [390, 1280]) {
+ for (const variant of ['all-visualizations', 'custom-brand', 'large-text']) {
+ test(`${width}: ${variant} keeps complete attribution within every visualization`, async ({ page }) => {
+ await page.setViewportSize({ width, height: 950 });
+ await page.goto(`/?story=gallery--attribution--${variant}&mode=preview`);
+ await expect(page.locator('[data-attribution-kind]')).toHaveCount(7);
+ await page.evaluate(async () => {
+ await document.fonts.ready;
+ await new Promise(requestAnimationFrame);
+ await new Promise(requestAnimationFrame);
+ });
+ for (const kind of ['chart', 'map', 'barlist', 'sankey', 'tilemap', 'table', 'graph']) {
+ const card = page.locator(`[data-attribution-kind=${kind}]`);
+ await expect(card.locator('.oc-source')).toContainText('2020–2025');
+ // SVG line breaks are separate tspans, without intervening text nodes.
+ await expect(card.locator('.oc-byline')).toContainText(/Open\s*research\s*team/);
+ await expect(card.locator('.oc-footer')).toContainText(/Methods\s*and\s*coverage/);
+ if (variant === 'all-visualizations') {
+ await expect(card.locator('.oc-watermark')).toHaveCount(1);
+ } else {
+ await expect(card.locator('.oc-brand')).toContainText(/Open\s*methods/);
+ await expect(card.locator('.oc-watermark')).toHaveCount(0);
+ }
+ const overflow = await card.evaluate((element) => {
+ const selectors = '.oc-source, .oc-byline, .oc-footer, .oc-brand, .oc-watermark';
+ return [...element.querySelectorAll(selectors)].flatMap((text) => {
+ const box = text.getBoundingClientRect();
+ const frame = text.ownerSVGElement!.getBoundingClientRect();
+ return box.left < frame.left - 1 || box.right > frame.right + 1 ||
+ box.top < frame.top - 1 || box.bottom > frame.bottom + 1
+ ? [{ text: text.textContent, kind: element.getAttribute('data-attribution-kind') }]
+ : [];
+ });
+ });
+ expect(overflow, kind).toEqual([]);
+ }
+ });
+ }
+}
diff --git a/e2e/chart-map-design/geo-labels.spec.ts b/e2e/chart-map-design/geo-labels.spec.ts
new file mode 100644
index 00000000..10faffde
--- /dev/null
+++ b/e2e/chart-map-design/geo-labels.spec.ts
@@ -0,0 +1,201 @@
+import { expect, type Locator, type Page, test } from '@playwright/test';
+import { waitForResizeSettled } from '../settle';
+
+const story = '/?story=gallery--geographic-labels--places-and-notes&mode=preview';
+async function settle(page: Page) {
+ await page.evaluate(async () => {
+ await document.fonts.ready;
+ await new Promise(requestAnimationFrame);
+ await new Promise(requestAnimationFrame);
+ });
+}
+
+async function exportGeometry(host: Locator) {
+ await host.dispatchEvent('fixture-export');
+ return host.evaluate((element) => {
+ const doc = new DOMParser().parseFromString(element.dataset.export!, 'image/svg+xml');
+ return [...doc.querySelectorAll('[data-geo-label-id], [data-geo-note-id]')].map((node) => ({
+ id: node.getAttribute('data-geo-label-id') ?? node.getAttribute('data-geo-note-id'),
+ content: node.textContent,
+ geometry: [node, ...node.querySelectorAll('*')].map((el) =>
+ ['x', 'y', 'cx', 'cy', 'd', 'transform', 'font-size'].map((attr) => el.getAttribute(attr)),
+ ),
+ }));
+ });
+}
+
+for (const width of [390, 1280]) {
+ test(`${width}: complete inventory is independent of fixed-height map and survives replacement`, async ({ page }) => {
+ await page.setViewportSize({ width, height: 950 });
+ await page.goto(story);
+ const host = page.getByTestId('geo-label-host');
+ const disclosure = page.getByTestId('geo-label-disclosure');
+ await expect(host.locator('svg.oc-map')).toBeVisible();
+ await settle(page);
+ const height = await host.evaluate((el) => el.getBoundingClientRect().height);
+ const viewBox = await host.locator('svg.oc-map').getAttribute('viewBox');
+ const camera = await host.getAttribute('data-camera');
+ const hidden = host.locator('.oc-map-label-inventory');
+ await expect(hidden).toContainText('California coast');
+ await expect(hidden).toContainText('A place without a data row');
+ await expect(hidden).toContainText('Austin detail at a closer zoom');
+ await expect(hidden).toContainText('Visible at another zoom level');
+ await disclosure.getByText('Map places and notes', { exact: true }).click();
+ await expect(disclosure).toContainText('A place without a data row');
+ expect(await host.evaluate((el) => el.getBoundingClientRect().height)).toBe(height);
+ expect(await host.locator('svg.oc-map').getAttribute('viewBox')).toBe(viewBox);
+ expect(await host.getAttribute('data-camera')).toBe(camera);
+ await page.getByRole('button', { name: 'Move place list' }).click();
+ await expect(disclosure.locator('details')).toHaveCount(0);
+ await expect(page.getByTestId('geo-label-alternate').locator('details')).toHaveCount(1);
+ expect(await host.getAttribute('data-camera')).toBe(camera);
+ await page.getByRole('button', { name: 'Update a place name' }).click();
+ await expect(hidden).toContainText('Texas updated');
+ expect(await host.evaluate((el) => el.getBoundingClientRect().height)).toBe(height);
+ const described = await host.locator('svg.oc-map').evaluate((svg) =>
+ (svg.getAttribute('aria-describedby') ?? '').split(/\s+/).some((id) => {
+ const node = document.getElementById(id);
+ return node?.textContent?.includes('Texas updated');
+ }),
+ );
+ expect(described).toBe(true);
+ });
+
+ test(`${width}: camera history does not affect settled geometry or standalone inventory`, async ({ page }) => {
+ await page.setViewportSize({ width, height: 950 });
+ await page.goto(story);
+ const host = page.getByTestId('geo-label-host');
+ await expect(host.locator('[data-geo-label-id]').first()).toBeVisible();
+ await settle(page);
+ const before = await exportGeometry(host);
+ expect(before.length).toBeGreaterThan(0);
+ const camera = JSON.parse((await host.getAttribute('data-camera'))!);
+ await page.getByRole('button', { name: 'Show Texas' }).click();
+ await settle(page);
+ await host.dispatchEvent('fixture-camera', { detail: camera });
+ await settle(page);
+ expect(await exportGeometry(host)).toEqual(before);
+ const standalone = await host.getAttribute('data-export');
+ expect(standalone).toContain('A place without a data row');
+ expect(standalone).toContain('Austin detail at a closer zoom');
+ expect(standalone).toContain('California coast');
+ expect(standalone).not.toContain('foreignObject');
+ const labelsInside = await host.evaluate((el) => {
+ const svg = el.querySelector('svg.oc-map')!.getBoundingClientRect();
+ return [...el.querySelectorAll('[data-geo-label-id]')].every((label) => {
+ const r = label.getBoundingClientRect();
+ return r.left >= svg.left - 1 && r.right <= svg.right + 1 &&
+ r.top >= svg.top - 1 && r.bottom <= svg.bottom + 1;
+ });
+ });
+ expect(labelsInside).toBe(true);
+ });
+}
+
+test('invalid updates preserve the valid view and inventory', async ({ page }) => {
+ await page.goto(story);
+ const host = page.getByTestId('geo-label-host');
+ await expect(host.locator('[data-geo-label-id]').first()).toBeVisible();
+ await settle(page);
+ const geometry = await exportGeometry(host);
+ const camera = await host.getAttribute('data-camera');
+ await host.dispatchEvent('fixture-invalid-update');
+ await expect(host).toHaveAttribute('data-validation-error', /duplicate|unique/i);
+ expect(await exportGeometry(host)).toEqual(geometry);
+ expect(await host.getAttribute('data-camera')).toBe(camera);
+ await expect(host.locator('.oc-map-label-inventory')).toContainText('California coast');
+});
+
+test('multiple maps keep accessible and SVG reference IDs separate', async ({ page }) => {
+ await page.goto('/?story=gallery--geographic-labels--multiple-maps&mode=preview');
+ await expect(page.getByTestId('geo-label-second').locator('svg.oc-map')).toBeVisible();
+ await settle(page);
+ const result = await page.evaluate(() => {
+ const hosts = [document.querySelector('[data-testid=geo-label-host]')!,
+ document.querySelector('[data-testid=geo-label-second]')!];
+ const ids = hosts.flatMap((host) => [...host.querySelectorAll('[id]')].map((n) => n.id));
+ const descriptionsLocal = hosts.every((host) => {
+ const svg = host.querySelector('svg.oc-map')!;
+ const refs = (svg.getAttribute('aria-describedby') ?? '').split(/\s+/).filter(Boolean);
+ return refs.length > 0 && refs.every((id) => host.contains(document.getElementById(id)));
+ });
+ return { unique: new Set(ids).size === ids.length, descriptionsLocal };
+ });
+ expect(result).toEqual({ unique: true, descriptionsLocal: true });
+});
+
+test('zoom keeps text at screen size and map controls clear of the size key', async ({ page }) => {
+ await page.setViewportSize({ width: 390, height: 950 });
+ await page.goto(story);
+ const host = page.getByTestId('geo-label-host');
+ await expect(host.locator('[data-geo-label-id]').first()).toBeVisible();
+ await settle(page);
+ // Fonts and the initial debounced ResizeObserver can replace the SVG after
+ // its first label is visible. Wait for that render, then read one DOM snapshot.
+ await expect(host).toHaveAttribute('data-oc-fonts-state', 'ready');
+ await waitForResizeSettled(host);
+ await expect(host.locator('svg text').filter({ hasText: /^Metro population$/ })).toBeVisible();
+ const { controls, title } = await host.evaluate((element) => ({
+ controls: element.querySelector('.oc-map-zoom-controls')?.getBoundingClientRect().toJSON() ?? null,
+ title: [...element.querySelectorAll('svg text')]
+ .find((text) => text.textContent === 'Metro population')?.getBoundingClientRect().toJSON() ?? null,
+ }));
+ expect(controls).not.toBeNull();
+ expect(title).not.toBeNull();
+ expect(controls!.x >= title!.x + title!.width || title!.x >= controls!.x + controls!.width ||
+ controls!.y >= title!.y + title!.height || title!.y >= controls!.y + controls!.height).toBe(true);
+ const sizes = () => host.locator('[data-geo-label-id] text').evaluateAll((texts) =>
+ Object.fromEntries(texts.map((text) => [text.textContent, text.getBoundingClientRect().height])),
+ );
+ const before = await sizes();
+ await page.getByRole('button', { name: 'Show Texas' }).click();
+ await settle(page);
+ const after = await sizes();
+ const shared = Object.keys(after).filter((name) => before[name] !== undefined);
+ expect(shared.length).toBeGreaterThan(0);
+ for (const name of shared) expect(after[name]).toBeCloseTo(before[name], 1);
+});
+
+test('schematic holes and unresolved anchors retain names without growing the map', async ({ page }) => {
+ await page.goto('/?story=gallery--geographic-labels--islands-and-holes&mode=preview');
+ const host = page.getByTestId('geo-geometry-host');
+ await expect(host.locator('[data-geo-label-id]').first()).toBeVisible();
+ const inventory = host.locator('.oc-map-label-inventory');
+ await expect(inventory).toContainText('Around the lake');
+ await expect(inventory).toContainText('Island group');
+ await expect(inventory).toContainText('A place outside this supplied geography');
+ const height = await host.evaluate((el) => el.getBoundingClientRect().height);
+ await page.getByRole('button', { name: 'Show the small island' }).click();
+ await settle(page);
+ await expect(host.locator('[data-geo-label-id="feature:islands"]')).toBeVisible();
+ expect(await host.evaluate((el) => el.getBoundingClientRect().height)).toBe(height);
+ await page.getByRole('button', { name: 'All islands' }).click();
+ await settle(page);
+ await expect(host.locator('[data-geo-label-id="feature:ring"]')).toBeVisible();
+});
+
+for (const width of [390, 1280]) {
+ test(`${width}: multiple-map gallery controls remain valid and independent`, async ({ page }) => {
+ const errors: string[] = [];
+ page.on('pageerror', (error) => errors.push(error.message));
+ await page.setViewportSize({ width, height: 950 });
+ await page.goto('/?story=gallery--geographic-labels--multiple-maps&mode=preview');
+ const first = page.getByTestId('geo-label-host');
+ const second = page.getByTestId('geo-label-second');
+ await expect(second.locator('svg.oc-map')).toBeVisible();
+ await expect(page.getByRole('button', { name: 'Try an invalid update' })).toHaveCount(0);
+ const secondCamera = await second.getAttribute('data-camera');
+ const secondGeometry = await exportGeometry(second);
+ for (const name of ['Show Texas', 'Full map', 'Update a place name', 'Move place list']) {
+ await page.getByRole('button', { name, exact: true }).click();
+ await settle(page);
+ await expect(first.locator('svg.oc-map')).toBeVisible();
+ expect(await second.getAttribute('data-camera')).toBe(secondCamera);
+ expect(await exportGeometry(second)).toEqual(secondGeometry);
+ }
+ await expect(first.locator('.oc-map-label-inventory')).toContainText('Texas updated');
+ await expect(second.locator('.oc-map-label-inventory')).toContainText('Texas triangle');
+ await expect(page.getByText(/Error: Invalid spec/)).toHaveCount(0);
+ expect(errors).toEqual([]);
+ });
+}
diff --git a/e2e/chart-map-design/geo-views.spec.ts b/e2e/chart-map-design/geo-views.spec.ts
new file mode 100644
index 00000000..bd0cce5d
--- /dev/null
+++ b/e2e/chart-map-design/geo-views.spec.ts
@@ -0,0 +1,213 @@
+import { expect, type Locator, type Page, test } from '@playwright/test';
+
+const story = (name: string) => `/?story=gallery--geographic-views--${name}&mode=preview`;
+async function settle(page: Page) {
+ await page.evaluate(async () => {
+ await document.fonts.ready;
+ await new Promise(requestAnimationFrame);
+ await new Promise(requestAnimationFrame);
+ });
+}
+async function exported(host: Locator) {
+ await host.dispatchEvent('fixture-export');
+ return host.getAttribute('data-export');
+}
+async function cameras(host: Locator) {
+ return JSON.parse((await host.getAttribute('data-cameras'))!);
+}
+for (const width of [390, 1280]) {
+ test(`${width}: Texas detail shares immutable fit and preserves Austin/RGV point geometry`, async ({
+ page,
+ }) => {
+ await page.setViewportSize({ width, height: 1000 });
+ await page.goto(story('texas-and-austin'));
+ const host = page.getByTestId('geo-texas-views');
+ await expect(host.locator('svg.oc-map')).toBeVisible();
+ await settle(page);
+ const points = await host.getAttribute('data-points');
+ expect(JSON.parse(points!)).toHaveLength(4);
+ const detail = host.locator('[data-geo-layer-id="travis-tracts"]');
+ await expect(detail).toHaveAttribute('display', 'none');
+ await page.getByRole('button', { name: 'Austin detail', exact: true }).click();
+ await settle(page);
+ await expect(detail).not.toHaveAttribute('display', 'none');
+ expect(await detail.locator('path').count()).toBe(290);
+ expect(await host.getAttribute('data-points')).toBe(points);
+ await page.getByRole('button', { name: 'Toggle tract context', exact: true }).click();
+ await settle(page);
+ await expect(host.locator('[data-geo-layer-id="travis-tracts"]')).toHaveCount(0);
+ expect(await host.getAttribute('data-points')).toBe(points);
+ await page.getByRole('button', { name: 'Toggle tract context', exact: true }).click();
+ await settle(page);
+ expect(await host.getAttribute('data-points')).toBe(points);
+ const svg = await exported(host);
+ expect(svg).toContain('travis-tracts');
+ expect(svg).not.toContain('foreignObject');
+ });
+ test(`${width}: explicit inset coverage has readable separate-scale captions and independent cameras`, async ({
+ page,
+ }) => {
+ await page.setViewportSize({ width, height: 1000 });
+ await page.goto(story('separate-scales'));
+ const host = page.getByTestId('geo-inset-views');
+ await expect(host.locator('svg.oc-map')).toBeVisible();
+ await settle(page);
+ await expect(host.locator('[data-geo-view-id]')).toHaveCount(3);
+ const views = JSON.parse((await host.getAttribute('data-views'))!);
+ const root = views.find((v: { id: string }) => v.id === 'root');
+ expect(root.features).not.toContain('02');
+ expect(root.features).not.toContain('15');
+ expect(views.find((v: { id: string }) => v.id === 'alaska').features).toEqual(['02']);
+ expect(views.find((v: { id: string }) => v.id === 'hawaii').features).toEqual(['15']);
+ for (const id of ['alaska', 'hawaii']) {
+ const view = host.locator(`[data-geo-view-id="${id}"]`);
+ const captions = await view.locator(':scope > text').allTextContents();
+ expect(captions.join(' ')).toMatch(/Alaska|Hawaii/);
+ expect(captions.join(' ')).toMatch(/separate scale/i);
+ }
+ const collisions = await host.evaluate((element) => {
+ const controls = [...element.querySelectorAll('.oc-map-zoom-controls')].map((node) =>
+ node.getBoundingClientRect(),
+ );
+ return [...element.querySelectorAll('[data-geo-label-id]')]
+ .filter((node) => {
+ const label = node.getBoundingClientRect();
+ return controls.some(
+ (control) =>
+ Math.min(label.right, control.right) - Math.max(label.left, control.left) > 0.5 &&
+ Math.min(label.bottom, control.bottom) - Math.max(label.top, control.top) > 0.5,
+ );
+ })
+ .map((node) => node.textContent);
+ });
+ expect(collisions).toEqual([]);
+ const before = await cameras(host);
+ await host.evaluate(
+ (element, camera) => {
+ element.dispatchEvent(
+ new CustomEvent('fixture-camera', { detail: { viewId: 'alaska', camera } }),
+ );
+ },
+ { ...before.alaska, k: before.alaska.k * 1.5 },
+ );
+ await settle(page);
+ const after = await cameras(host);
+ expect(after.root).toEqual(before.root);
+ expect(after.hawaii).toEqual(before.hawaii);
+ expect(after.alaska.k).toBeGreaterThan(before.alaska.k);
+ await page.getByRole('button', { name: 'Reset main view', exact: true }).click();
+ await settle(page);
+ expect((await cameras(host)).alaska).toEqual(after.alaska);
+ const svg = await exported(host);
+ expect(svg).toContain('Alaska');
+ expect(svg).toContain('Hawaii');
+ expect(svg).toMatch(/Separate scale/i); // The accessible coverage description is a single string.
+ });
+}
+test('incompatible identity source planes reject updates while preserving the last valid view', async ({
+ page,
+}) => {
+ await page.goto(story('texas-and-austin'));
+ const host = page.getByTestId('geo-texas-views');
+ await expect(host.locator('svg.oc-map')).toBeVisible();
+ await settle(page);
+ const before = await host.getAttribute('data-points'),
+ camera = await host.getAttribute('data-cameras');
+ await host.dispatchEvent('fixture-invalid-plane');
+ await expect(host).toHaveAttribute('data-validation-error', /sourcePlane|source plane/i);
+ expect(await host.getAttribute('data-points')).toBe(before);
+ expect(await host.getAttribute('data-cameras')).toBe(camera);
+ await expect(host.locator('svg.oc-map')).toBeVisible();
+ for (const scope of ['view', 'layer'] as const) {
+ await host.evaluate((element, scope) => {
+ element.dispatchEvent(new CustomEvent('fixture-invalid-scope', { detail: scope }));
+ }, scope);
+ await expect(host).toHaveAttribute('data-scope-error', scope === 'view' ? /view/i : /layer/i);
+ expect(await host.getAttribute('data-points')).toBe(before);
+ expect(await host.getAttribute('data-cameras')).toBe(camera);
+ }
+});
+test('two inset maps export independently scoped clips with resolvable references', async ({
+ page,
+}) => {
+ await page.goto(story('two-maps'));
+ const first = page.getByTestId('geo-inset-first'),
+ second = page.getByTestId('geo-inset-second');
+ await expect(first.locator('svg.oc-map')).toBeVisible();
+ await expect(second.locator('svg.oc-map')).toBeVisible();
+ await settle(page);
+ await exported(first);
+ await exported(second);
+ const result = await page.evaluate(() => {
+ const strings = ['geo-inset-first', 'geo-inset-second'].map(
+ (id) => document.querySelector(`[data-testid="${id}"]`)!.dataset.export!,
+ );
+ const docs = strings.map((svg) => new DOMParser().parseFromString(svg, 'image/svg+xml'));
+ const ids = docs.flatMap((doc) => [...doc.querySelectorAll('[id]')].map((node) => node.id));
+ const unresolved = docs.flatMap((doc) =>
+ [...doc.querySelectorAll('*')].flatMap((node) =>
+ [...node.attributes].flatMap((attr) =>
+ [...attr.value.matchAll(/url\(#([^)]*)\)/g)]
+ .filter((match) => !doc.getElementById(match[1]))
+ .map((match) => match[1]),
+ ),
+ ),
+ );
+ return {
+ ids,
+ unresolved,
+ viewCounts: docs.map((doc) => doc.querySelectorAll('[data-geo-view-id]').length),
+ };
+ });
+ expect(result.viewCounts).toEqual([3, 3]);
+ expect(new Set(result.ids).size).toBe(result.ids.length);
+ expect(result.unresolved).toEqual([]);
+});
+
+test('one Alaska observation pins both explicitly duplicated view representations', async ({
+ page,
+}) => {
+ await page.goto(story('duplicated-place'));
+ const host = page.getByTestId('geo-duplicate-view');
+ await expect(host.locator('[data-geo-view-id]')).toHaveCount(3);
+ await settle(page);
+ await page.getByRole('button', { name: 'Pin Alaska', exact: true }).click();
+ await expect(page.getByTestId('geo-view-reader-panel')).toContainText('Alaska');
+ const selection = JSON.parse((await host.getAttribute('data-reader'))!);
+ expect(selection.pinned).toHaveLength(1);
+ expect(selection.pinned[0].observation).toBe('feature:02');
+ await expect(host.locator('[data-oc-reader-overlay] path')).toHaveCount(2);
+});
+
+for (const width of [390, 1280]) {
+ test(`${width}: larger inset captions remain inside their measured frames`, async ({ page }) => {
+ await page.setViewportSize({ width, height: 1000 });
+ await page.goto(story('separate-scales'));
+ const host = page.getByTestId('geo-inset-views');
+ await page.getByRole('button', { name: 'Enlarge caption text', exact: true }).click();
+ await expect(host.locator('[data-geo-view-id]')).toHaveCount(3);
+ await settle(page);
+ const overflow = await host.evaluate((element) =>
+ [...element.querySelectorAll('[data-geo-view-id]:not([data-geo-view-id="root"])')].flatMap(
+ (view) => {
+ const frame = view.querySelector(':scope > rect')!.getBoundingClientRect();
+ return [...view.querySelectorAll(':scope > text')]
+ .filter((text) => {
+ const box = text.getBoundingClientRect();
+ return (
+ box.left < frame.left - 0.5 ||
+ box.right > frame.right + 0.5 ||
+ box.top < frame.top - 0.5 ||
+ box.bottom > frame.bottom + 0.5
+ );
+ })
+ .map((text) => text.textContent);
+ },
+ ),
+ );
+ expect(overflow).toEqual([]);
+ expect(
+ await host.locator('[data-geo-view-id="alaska"] > text').first().getAttribute('font-size'),
+ ).toBe('24');
+ });
+}
diff --git a/e2e/chart-map-design/reader.spec.ts b/e2e/chart-map-design/reader.spec.ts
new file mode 100644
index 00000000..d1710036
--- /dev/null
+++ b/e2e/chart-map-design/reader.spec.ts
@@ -0,0 +1,102 @@
+import { expect, type Locator, type Page, test } from '@playwright/test';
+
+async function settle(page: Page) {
+ await page.evaluate(async () => {
+ await document.fonts.ready;
+ await new Promise(requestAnimationFrame);
+ await new Promise(requestAnimationFrame);
+ });
+}
+async function pinNext(host: Locator) {
+ await host.focus();
+ await host.press('ArrowRight');
+ await host.press('Enter');
+}
+
+for (const width of [390, 1280]) {
+ test(`${width}: date slices preserve sparse observations and reconcile keyed updates`, async ({ page }) => {
+ await page.setViewportSize({ width, height: 950 });
+ await page.goto('/?story=gallery--reader-comparison--dates&mode=preview');
+ const host = page.locator('[data-reader-chart]');
+ const panel = page.getByRole('complementary', { name: 'Date comparison', exact: true });
+ await expect(host.locator('svg.oc-chart')).toBeVisible();
+ await settle(page);
+ await pinNext(host);
+ await pinNext(host);
+ await expect(panel.getByRole('heading', { name: 'A: 2022' })).toBeVisible();
+ await expect(panel.getByRole('heading', { name: 'B: 2023' })).toBeVisible();
+ await expect(panel).toContainText('No observation');
+ await expect(panel).toContainText('3 percentage points');
+ await expect(panel).toContainText('Change unavailable');
+ await pinNext(host);
+ await expect(panel.getByRole('heading', { name: 'A: 2022' })).toBeVisible();
+ await expect(panel.getByRole('heading', { name: 'B: 2024' })).toBeVisible();
+ await page.getByRole('button', { name: 'Reorder observations' }).click();
+ await expect(panel.getByRole('heading', { name: 'B: 2024' })).toBeVisible();
+ await page.getByRole('button', { name: 'Remove 2024' }).click();
+ await expect(panel.getByRole('heading', { name: 'B: 2024' })).toHaveCount(0);
+ await expect(panel.getByRole('heading', { name: 'A: 2022' })).toBeVisible();
+ await page.getByRole('button', { name: 'Move comparison panel' }).click();
+ await expect(panel.locator('.oc-reader-panel')).toHaveCount(0);
+ const alternate = page.getByRole('complementary', { name: 'Alternate date comparison' });
+ await expect(alternate.getByRole('heading', { name: 'A: 2022' })).toBeVisible();
+ await alternate.getByRole('button', { name: 'Clear selection' }).click();
+ await expect(host.locator('.oc-reader-guides [data-reader-slice]')).toHaveCount(0);
+ });
+
+ test(`${width}: place search and pins do not change camera; exports omit transient selection`, async ({ page }) => {
+ await page.setViewportSize({ width, height: 950 });
+ await page.goto('/?story=gallery--reader-comparison--places&mode=preview');
+ const host = page.locator('[data-reader-map]');
+ const companion = page.getByRole('complementary', { name: 'Find and rank places' });
+ const panel = page.getByRole('complementary', { name: 'Place comparison' });
+ await expect(host.locator('svg.oc-map')).toBeVisible();
+ await settle(page);
+ const camera = await host.getAttribute('data-camera');
+ const viewBox = await host.locator('svg.oc-map').getAttribute('viewBox');
+ await companion.getByRole('button', { name: /^1\. California: 42/ }).click();
+ await companion.getByRole('button', { name: /^2\. Texas: 31/ }).click();
+ await expect(panel.getByRole('heading', { name: 'A: California' })).toBeVisible();
+ await expect(panel.getByRole('heading', { name: 'B: Texas' })).toBeVisible();
+ await expect(panel).toContainText('percentage points');
+ expect(await host.getAttribute('data-camera')).toBe(camera);
+ expect(await host.locator('svg.oc-map').getAttribute('viewBox')).toBe(viewBox);
+ await host.dispatchEvent('fixture-export');
+ const exported = await host.getAttribute('data-export');
+ expect(exported).toContain('Regions per range');
+ expect(exported).not.toMatch(/oc-map-reader|oc-reader-panel|foreignObject/);
+ await companion.getByRole('searchbox', { name: 'Find a place' }).fill('Washington');
+ await companion.getByRole('button', { name: /Washington: 0/ }).click();
+ await expect(panel.getByRole('heading', { name: 'B: Washington' })).toBeVisible();
+ await expect(panel).toContainText('0 %');
+ await companion.getByRole('searchbox', { name: 'Find a place' }).fill('Alabama');
+ await companion.getByRole('button', { name: /^Alabama: No data/ }).click();
+ await expect(panel.getByRole('heading', { name: 'B: Alabama' })).toBeVisible();
+ await expect(panel).toContainText('No data');
+ await expect(panel).toContainText('A comparison needs two observed finite values.');
+ expect(await host.getAttribute('data-camera')).toBe(camera);
+ await companion.getByRole('button', { name: 'Show Alabama on map' }).click();
+ await expect(host).not.toHaveAttribute('data-camera', camera!);
+ });
+}
+
+test('map keyboard inspection and Escape leave the fixed frame intact', async ({ page }) => {
+ await page.goto('/?story=gallery--reader-comparison--places&mode=preview');
+ const host = page.locator('[data-reader-map]');
+ await expect(host.locator('svg.oc-map')).toBeVisible();
+ await settle(page);
+ const camera = await host.getAttribute('data-camera');
+ const inspect = host.getByRole('button', { name: /^Inspect places/ });
+ await inspect.focus();
+ await inspect.press('ArrowRight');
+ await inspect.press('Enter');
+ const panel = page.getByRole('complementary', { name: 'Place comparison' });
+ await expect(panel.locator('[data-reader-slot=A]')).toHaveCount(1);
+ await inspect.press('ArrowRight');
+ await inspect.press('Enter');
+ await expect(panel.locator('[data-reader-slot=B]')).toHaveCount(1);
+ expect(await host.getAttribute('data-camera')).toBe(camera);
+ await inspect.press('Escape');
+ await expect(panel.locator('[data-reader-slot]')).toHaveCount(0);
+ expect(await host.getAttribute('data-camera')).toBe(camera);
+});
diff --git a/e2e/invariants/canvas-alpha-parity.spec.ts b/e2e/invariants/canvas-alpha-parity.spec.ts
index 253ef3e9..9871d1fb 100644
--- a/e2e/invariants/canvas-alpha-parity.spec.ts
+++ b/e2e/invariants/canvas-alpha-parity.spec.ts
@@ -42,8 +42,19 @@ test('canvas composites stacked translucent points like SVG does', async ({ page
await page.waitForSelector('#parity-svg.oc-root svg.oc-chart');
await page.waitForSelector('#parity-canvas canvas.oc-mark-canvas');
await page.evaluate(() => document.fonts.ready);
- // The canvas layer paints on a rAF tick; give it a couple of frames to land.
- await page.waitForTimeout(200);
+ // The post-font recompile rebuilds the canvas layer, so wait for it first.
+ await page.waitForSelector('#parity-canvas[data-oc-fonts-state="ready"] canvas.oc-mark-canvas');
+ // The canvas layer paints on a rAF tick, all marks in one pass (the story
+ // sets `animation: false`, so the first paint is the final one). Any
+ // non-transparent pixel means that pass has landed.
+ await page.waitForFunction(() => {
+ const c = document.querySelector('#parity-canvas canvas.oc-mark-canvas') as HTMLCanvasElement | null;
+ const ctx = c?.getContext('2d');
+ if (!c || !ctx || c.width === 0 || c.height === 0) return false;
+ const { data } = ctx.getImageData(0, 0, c.width, c.height);
+ for (let i = 3; i < data.length; i += 4) if (data[i] !== 0) return true;
+ return false;
+ });
const sample = await page.evaluate(() => {
const markCanvas = document.querySelector(
diff --git a/e2e/invariants/known-bugs.spec.ts b/e2e/invariants/known-bugs.spec.ts
index 74a1dcd6..e7693016 100644
--- a/e2e/invariants/known-bugs.spec.ts
+++ b/e2e/invariants/known-bugs.spec.ts
@@ -1,4 +1,6 @@
import { expect, test } from '@playwright/test';
+import { waitForResizeSettled } from '../settle';
+import { DEFAULT_THEME } from '../../packages/core/src/theme/defaults';
/**
* Layout invariants that were once red-locked bugs and are now fixed.
@@ -20,7 +22,8 @@ test('rotated x-axis labels do not overlap source text', async ({ page }) => {
// Wait for the chart to render.
await page.waitForSelector('.oc-root');
await page.evaluate(() => document.fonts.ready);
- await page.waitForTimeout(100);
+ await page.waitForSelector('.oc-root[data-oc-fonts-state="ready"] svg.oc-chart:not(.oc-animate)');
+ await waitForResizeSettled(page.locator('.oc-root').first());
const overlap = await page.evaluate(() => {
// Collect bounding rects for all x-axis tick labels.
@@ -53,18 +56,14 @@ test('rotated x-axis labels do not overlap source text', async ({ page }) => {
// Primary invariant: the source text must never overlap the axis labels.
expect(overlap.gap, 'source text overlaps rotated x-axis labels').toBeGreaterThanOrEqual(0);
- // Secondary: the gap should stay a sane, readable size — roughly the theme's
- // chartToFooter spacing (default 8px) without a large dead band. The exact
- // value drifts by engine: Blink and WebKit place the bottom edge of a -45°
- // rotated glyph several pixels apart for the same layout, so we bound the
- // gap to a range that fits every engine (Chrome desktop ~3px, mobile WebKit
- // ~19px) rather than pinning it to a tight window it can't hold. The bottom
- // extent now reserves space from the real canvas-measured label width (axes
- // and planner agree on the measurer), which on WebKit-mobile measures the
- // rotated labels a few pixels wider than the old heuristic — a slightly more
- // conservative reservation, never an overlap.
+ // Keep the same 14px cross-engine glyph-bounds tolerance above the theme's
+ // footer spacing. v8 used 8px + 14px = 22px; v9 deliberately uses 12px +
+ // 14px = 26px. WebKit places the bottom of a rotated glyph several pixels
+ // higher than Blink for the same reserved extent. The no-overlap bound above
+ // stays strict; this upper bound still rejects a large accidental dead band.
+ const maxGap = DEFAULT_THEME.spacing.chartToFooter + 14;
expect(
overlap.gap,
- `gap between axis labels and source should be readable (0-22px), got ${overlap.gap.toFixed(1)}px`,
- ).toBeLessThanOrEqual(22);
+ `gap between axis labels and source should be readable (0-${maxGap}px), got ${overlap.gap.toFixed(1)}px`,
+ ).toBeLessThanOrEqual(maxGap);
});
diff --git a/e2e/invariants/library-interactions.spec.ts b/e2e/invariants/library-interactions.spec.ts
index d0bd518c..b44b5479 100644
--- a/e2e/invariants/library-interactions.spec.ts
+++ b/e2e/invariants/library-interactions.spec.ts
@@ -79,9 +79,13 @@ test('table: Escape wins over pending search, sort retains focus and priority ce
await search.press('Escape');
await expect(search).toHaveValue('');
await expect(host.locator('tbody')).toBeFocused();
- // Wait past the debounce and prove the stale query never returns.
+ // Prove the stale query never returns. The pending search is a 200ms
+ // setTimeout in table-mount.ts (handleSearchInput), scheduled before Escape.
+ // A page-side timer of the same delay scheduled now runs after it (equal
+ // delays run in scheduling order), so if the stale search were going to
+ // fire it has already re-rendered by the time this resolves.
await expect(host.locator('tbody tr')).toHaveCount(10);
- await page.waitForTimeout(250);
+ await page.evaluate(() => new Promise((resolve) => setTimeout(resolve, 200)));
await expect(host.locator('tbody tr')).toHaveCount(10);
const header = host.locator('th[data-column="state"]');
await header.focus();
diff --git a/e2e/invariants/rendered-invariants.spec.ts b/e2e/invariants/rendered-invariants.spec.ts
index 6e34e56c..859e0e56 100644
--- a/e2e/invariants/rendered-invariants.spec.ts
+++ b/e2e/invariants/rendered-invariants.spec.ts
@@ -1,4 +1,5 @@
import { expect, test } from '@playwright/test';
+import { waitForResizeSettled } from '../settle';
/**
* Layout invariant checks on real rendered charts.
@@ -25,7 +26,10 @@ for (const { name, slug } of stories) {
await page.goto(`/?story=${encodeURIComponent(slug)}&mode=preview`);
await page.waitForSelector('.oc-root');
await page.evaluate(() => document.fonts.ready);
- await page.waitForTimeout(100);
+ // Settled render: the post-font recompile ran and the entrance animation
+ // (which defers resizes) finished, then any debounced resize was handled.
+ await page.waitForSelector('.oc-root[data-oc-fonts-state="ready"] svg.oc-chart:not(.oc-animate)');
+ await waitForResizeSettled(page.locator('.oc-root').first());
const violations = await page.evaluate(() => {
const violations: string[] = [];
@@ -187,7 +191,10 @@ for (const { name, slug } of stories) {
await page.goto(`/?story=${encodeURIComponent(slug)}&mode=preview`);
await page.waitForSelector('.oc-root');
await page.evaluate(() => document.fonts.ready);
- await page.waitForTimeout(100);
+ // Settled render: the post-font recompile ran and the entrance animation
+ // (which defers resizes) finished, then any debounced resize was handled.
+ await page.waitForSelector('.oc-root[data-oc-fonts-state="ready"] svg.oc-chart:not(.oc-animate)');
+ await waitForResizeSettled(page.locator('.oc-root').first());
const violations = await page.evaluate(() => {
const violations: string[] = [];
@@ -261,7 +268,8 @@ test('map legend anatomy: us-states-light', async ({ page }) => {
);
await page.waitForSelector('svg.oc-map');
await page.evaluate(() => document.fonts.ready);
- await page.waitForTimeout(100);
+ await page.waitForSelector('.oc-map-root[data-oc-fonts-state="ready"] svg.oc-map:not(.oc-animate)');
+ await waitForResizeSettled(page.locator('.oc-map-root').first());
const violations = await page.evaluate(() => {
const violations: string[] = [];
@@ -321,7 +329,8 @@ test('sankey labels stay inside the svg: sankey-energy', async ({ page }) => {
await page.goto(`/?story=${encodeURIComponent('testing--fixtures-sankey--energy')}&mode=preview`);
await page.waitForSelector('svg.oc-sankey');
await page.evaluate(() => document.fonts.ready);
- await page.waitForTimeout(100);
+ await page.waitForSelector('.oc-sankey-root[data-oc-fonts-state="ready"] svg.oc-sankey:not(.oc-animate)');
+ await waitForResizeSettled(page.locator('.oc-sankey-root').first());
const violations = await page.evaluate(() => {
const violations: string[] = [];
@@ -372,7 +381,8 @@ test('no-data features render a hatch pattern: world-diverging-quantize', async
);
await page.waitForSelector('svg.oc-map');
await page.evaluate(() => document.fonts.ready);
- await page.waitForTimeout(100);
+ await page.waitForSelector('.oc-map-root[data-oc-fonts-state="ready"] svg.oc-map:not(.oc-animate)');
+ await waitForResizeSettled(page.locator('.oc-map-root').first());
const fills = await page.evaluate(() => {
const svg = document.querySelector('svg.oc-map');
diff --git a/e2e/settle.ts b/e2e/settle.ts
new file mode 100644
index 00000000..429d0429
--- /dev/null
+++ b/e2e/settle.ts
@@ -0,0 +1,37 @@
+import type { Locator } from '@playwright/test';
+
+/**
+ * Mirrors DEBOUNCE_MS in packages/vanilla/src/resize-observer.ts. If that
+ * debounce grows, raise this to match or the wait below stops covering it.
+ */
+const RESIZE_DEBOUNCE_MS = 16;
+
+/**
+ * Resolve once the chart mount has handled every ResizeObserver notification
+ * for `host`, including the echo of any render those notifications caused.
+ *
+ * The observer installed here is created after the chart's own, so within one
+ * notification the chart schedules its debounce timer first. Timers with equal
+ * delay run in scheduling order, so when ours fires the chart's has already
+ * run and any render it triggered is in the DOM. A render that changes the
+ * host's size produces a new notification, which re-arms ours. The promise
+ * therefore resolves only when the host is size-stable; a resize feedback loop
+ * never resolves and the test times out.
+ */
+export async function waitForResizeSettled(host: Locator): Promise {
+ await host.evaluate(
+ (el, debounceMs) =>
+ new Promise((resolve) => {
+ let timer: ReturnType | undefined;
+ const observer = new ResizeObserver(() => {
+ clearTimeout(timer);
+ timer = setTimeout(() => {
+ observer.disconnect();
+ resolve();
+ }, debounceMs);
+ });
+ observer.observe(el);
+ }),
+ RESIZE_DEBOUNCE_MS,
+ );
+}
diff --git a/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/bar-vertical-mobile-visual-mobile-darwin.png b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/bar-vertical-mobile-visual-mobile-darwin.png
index e044fb38..42c20947 100644
Binary files a/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/bar-vertical-mobile-visual-mobile-darwin.png and b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/bar-vertical-mobile-visual-mobile-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/dashboard-kpi-tile-mobile-visual-mobile-darwin.png b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/dashboard-kpi-tile-mobile-visual-mobile-darwin.png
index 64e1ff64..81ce6f7a 100644
Binary files a/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/dashboard-kpi-tile-mobile-visual-mobile-darwin.png and b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/dashboard-kpi-tile-mobile-visual-mobile-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/line-multi-series-mobile-visual-mobile-darwin.png b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/line-multi-series-mobile-visual-mobile-darwin.png
index 9d311e3d..52aedcfd 100644
Binary files a/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/line-multi-series-mobile-visual-mobile-darwin.png and b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/line-multi-series-mobile-visual-mobile-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-auto-height-chrome-growth-mobile-visual-mobile-darwin.png b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-auto-height-chrome-growth-mobile-visual-mobile-darwin.png
index 9b978653..2dcc9f6e 100644
Binary files a/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-auto-height-chrome-growth-mobile-visual-mobile-darwin.png and b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-auto-height-chrome-growth-mobile-visual-mobile-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-chrome-layout-grow-mobile-visual-mobile-darwin.png b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-chrome-layout-grow-mobile-visual-mobile-darwin.png
index cbeb9e22..bb47b47b 100644
Binary files a/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-chrome-layout-grow-mobile-visual-mobile-darwin.png and b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-chrome-layout-grow-mobile-visual-mobile-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-chrome-max-lines-mobile-visual-mobile-darwin.png b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-chrome-max-lines-mobile-visual-mobile-darwin.png
index 9eb1e1be..95aa1dde 100644
Binary files a/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-chrome-max-lines-mobile-visual-mobile-darwin.png and b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-chrome-max-lines-mobile-visual-mobile-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-grouped-bars-many-rows-mobile-visual-mobile-darwin.png b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-grouped-bars-many-rows-mobile-visual-mobile-darwin.png
index 40828c8e..5a678e49 100644
Binary files a/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-grouped-bars-many-rows-mobile-visual-mobile-darwin.png and b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-grouped-bars-many-rows-mobile-visual-mobile-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-grouped-bars-sparse-ticks-mobile-visual-mobile-darwin.png b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-grouped-bars-sparse-ticks-mobile-visual-mobile-darwin.png
index f029598f..0fdc98a4 100644
Binary files a/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-grouped-bars-sparse-ticks-mobile-visual-mobile-darwin.png and b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-grouped-bars-sparse-ticks-mobile-visual-mobile-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-grouped-columns-labels-all-mobile-visual-mobile-darwin.png b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-grouped-columns-labels-all-mobile-visual-mobile-darwin.png
index 155e8764..1acafc63 100644
Binary files a/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-grouped-columns-labels-all-mobile-visual-mobile-darwin.png and b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-grouped-columns-labels-all-mobile-visual-mobile-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-inline-y-title-mobile-visual-mobile-darwin.png b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-inline-y-title-mobile-visual-mobile-darwin.png
index bfabfd77..7499a086 100644
Binary files a/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-inline-y-title-mobile-visual-mobile-darwin.png and b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-inline-y-title-mobile-visual-mobile-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-long-title-mobile-visual-mobile-darwin.png b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-long-title-mobile-visual-mobile-darwin.png
index ba5900e5..632b42fe 100644
Binary files a/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-long-title-mobile-visual-mobile-darwin.png and b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-long-title-mobile-visual-mobile-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-one-wide-x-label-large-ticks-mobile-visual-mobile-darwin.png b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-one-wide-x-label-large-ticks-mobile-visual-mobile-darwin.png
index a0e53223..2b524f0b 100644
Binary files a/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-one-wide-x-label-large-ticks-mobile-visual-mobile-darwin.png and b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-one-wide-x-label-large-ticks-mobile-visual-mobile-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-one-wide-x-label-mobile-visual-mobile-darwin.png b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-one-wide-x-label-mobile-visual-mobile-darwin.png
index f66f527c..818dc688 100644
Binary files a/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-one-wide-x-label-mobile-visual-mobile-darwin.png and b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-one-wide-x-label-mobile-visual-mobile-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-reference-lane-overflow-mobile-visual-mobile-darwin.png b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-reference-lane-overflow-mobile-visual-mobile-darwin.png
new file mode 100644
index 00000000..0c826cd6
Binary files /dev/null and b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-reference-lane-overflow-mobile-visual-mobile-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-uniform-short-x-labels-mobile-visual-mobile-darwin.png b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-uniform-short-x-labels-mobile-visual-mobile-darwin.png
index f2fae404..a2e82093 100644
Binary files a/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-uniform-short-x-labels-mobile-visual-mobile-darwin.png and b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/mr-uniform-short-x-labels-mobile-visual-mobile-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/stacked-column-mobile-visual-mobile-darwin.png b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/stacked-column-mobile-visual-mobile-darwin.png
index 11ce198e..b771509b 100644
Binary files a/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/stacked-column-mobile-visual-mobile-darwin.png and b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/stacked-column-mobile-visual-mobile-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/table-cards-mobile-visual-mobile-darwin.png b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/table-cards-mobile-visual-mobile-darwin.png
index 5fca2dfd..57eb01c6 100644
Binary files a/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/table-cards-mobile-visual-mobile-darwin.png and b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/table-cards-mobile-visual-mobile-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/v9-crowded-callouts-mobile-visual-mobile-darwin.png b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/v9-crowded-callouts-mobile-visual-mobile-darwin.png
new file mode 100644
index 00000000..f6f2afb8
Binary files /dev/null and b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/v9-crowded-callouts-mobile-visual-mobile-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/v9-curves-mobile-visual-mobile-darwin.png b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/v9-curves-mobile-visual-mobile-darwin.png
new file mode 100644
index 00000000..82e4eb5f
Binary files /dev/null and b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/v9-curves-mobile-visual-mobile-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/v9-notes-sizing-mobile-visual-mobile-darwin.png b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/v9-notes-sizing-mobile-visual-mobile-darwin.png
new file mode 100644
index 00000000..0e45386c
Binary files /dev/null and b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/v9-notes-sizing-mobile-visual-mobile-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/v9-reference-labels-mobile-visual-mobile-darwin.png b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/v9-reference-labels-mobile-visual-mobile-darwin.png
new file mode 100644
index 00000000..3e3d747b
Binary files /dev/null and b/e2e/visual/__screenshots__/stories-mobile.spec.ts-snapshots/v9-reference-labels-mobile-visual-mobile-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/a11y-pattern-fills-compact-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/a11y-pattern-fills-compact-visual-darwin.png
index a39c7c5d..3f64e0b0 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/a11y-pattern-fills-compact-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/a11y-pattern-fills-compact-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/a11y-pattern-fills-donut-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/a11y-pattern-fills-donut-visual-darwin.png
index dd7a36dd..bdc52824 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/a11y-pattern-fills-donut-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/a11y-pattern-fills-donut-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/a11y-pattern-fills-stacked-dark-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/a11y-pattern-fills-stacked-dark-visual-darwin.png
index 105f0989..d60de70f 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/a11y-pattern-fills-stacked-dark-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/a11y-pattern-fills-stacked-dark-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/a11y-pattern-fills-stacked-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/a11y-pattern-fills-stacked-visual-darwin.png
index 105f0989..d60de70f 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/a11y-pattern-fills-stacked-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/a11y-pattern-fills-stacked-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/agents-chat-analyst-final-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/agents-chat-analyst-final-visual-darwin.png
index eefec222..efbeb178 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/agents-chat-analyst-final-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/agents-chat-analyst-final-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/bar-horizontal-gradient-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/bar-horizontal-gradient-visual-darwin.png
index f470434e..ac41d4b8 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/bar-horizontal-gradient-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/bar-horizontal-gradient-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/bar-vertical-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/bar-vertical-visual-darwin.png
index 3b821bd6..68fcc65c 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/bar-vertical-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/bar-vertical-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/beeswarm-basic-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/beeswarm-basic-visual-darwin.png
index 161380df..7aba447f 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/beeswarm-basic-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/beeswarm-basic-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/beeswarm-grouped-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/beeswarm-grouped-visual-darwin.png
index faaa508c..750ddd59 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/beeswarm-grouped-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/beeswarm-grouped-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/beeswarm-sized-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/beeswarm-sized-visual-darwin.png
index 43281a1f..e0acb297 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/beeswarm-sized-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/beeswarm-sized-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/bump-constructors-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/bump-constructors-visual-darwin.png
index 5cd90596..1eeeff15 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/bump-constructors-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/bump-constructors-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/calendar-compact-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/calendar-compact-visual-darwin.png
index 4b58f440..9f9973ef 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/calendar-compact-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/calendar-compact-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/calendar-diverging-year-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/calendar-diverging-year-visual-darwin.png
index ada2c413..93663591 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/calendar-diverging-year-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/calendar-diverging-year-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/calendar-sequential-two-years-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/calendar-sequential-two-years-visual-darwin.png
index 327ae70b..3b5241ac 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/calendar-sequential-two-years-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/calendar-sequential-two-years-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/canvas-scatter-static-dark-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/canvas-scatter-static-dark-visual-darwin.png
index 671a3af2..8d82a350 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/canvas-scatter-static-dark-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/canvas-scatter-static-dark-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/canvas-scatter-static-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/canvas-scatter-static-visual-darwin.png
index da15eed8..ef31e3b3 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/canvas-scatter-static-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/canvas-scatter-static-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/chart-with-annotations-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/chart-with-annotations-visual-darwin.png
index 0eadb23d..a17738f9 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/chart-with-annotations-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/chart-with-annotations-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/chart-with-watermark-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/chart-with-watermark-visual-darwin.png
index 7a5ab7f9..7dcba538 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/chart-with-watermark-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/chart-with-watermark-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/color-legend-binned-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/color-legend-binned-visual-darwin.png
index 37023c44..975c4704 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/color-legend-binned-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/color-legend-binned-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/color-legend-dark-diverging-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/color-legend-dark-diverging-visual-darwin.png
index 73a6b504..7a003fe5 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/color-legend-dark-diverging-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/color-legend-dark-diverging-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/color-legend-gradient-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/color-legend-gradient-visual-darwin.png
index 81c16a9f..9a82c6ad 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/color-legend-gradient-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/color-legend-gradient-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/compact-bar-320px-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/compact-bar-320px-visual-darwin.png
index d2ee362b..42c951f4 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/compact-bar-320px-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/compact-bar-320px-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/compact-line-320px-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/compact-line-320px-visual-darwin.png
index 17bc5648..c94a5aa6 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/compact-line-320px-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/compact-line-320px-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/dark-mode-line-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/dark-mode-line-visual-darwin.png
index be9fef28..56a1dbf1 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/dark-mode-line-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/dark-mode-line-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/dashboard-kpi-tile-dark-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/dashboard-kpi-tile-dark-visual-darwin.png
index 75654db8..aac9407b 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/dashboard-kpi-tile-dark-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/dashboard-kpi-tile-dark-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/dashboard-kpi-tile-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/dashboard-kpi-tile-visual-darwin.png
index 023c6f55..edeb779b 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/dashboard-kpi-tile-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/dashboard-kpi-tile-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/dashboard-tiny-tile-140-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/dashboard-tiny-tile-140-visual-darwin.png
index 093111e6..33d5d8be 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/dashboard-tiny-tile-140-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/dashboard-tiny-tile-140-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/dashboard-tiny-tile-metrics-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/dashboard-tiny-tile-metrics-visual-darwin.png
index ddf2c6c8..2f755331 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/dashboard-tiny-tile-metrics-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/dashboard-tiny-tile-metrics-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/distribution-density-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/distribution-density-visual-darwin.png
index afdfd5ac..4aa73000 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/distribution-density-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/distribution-density-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/distribution-histogram-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/distribution-histogram-visual-darwin.png
index 0de425f2..479330c4 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/distribution-histogram-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/distribution-histogram-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/donut-center-label-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/donut-center-label-visual-darwin.png
index 9805c0f5..78e530ad 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/donut-center-label-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/donut-center-label-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/dual-axis-combo-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/dual-axis-combo-visual-darwin.png
index 8eb75628..946377df 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/dual-axis-combo-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/dual-axis-combo-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/election-donut-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/election-donut-visual-darwin.png
index 37c41093..855e3c07 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/election-donut-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/election-donut-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/election-results-bar-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/election-results-bar-visual-darwin.png
index 01a347d5..5d93c1bf 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/election-results-bar-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/election-results-bar-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/legend-bottom-right-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/legend-bottom-right-visual-darwin.png
index a882d969..9819539f 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/legend-bottom-right-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/legend-bottom-right-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/legend-bottom-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/legend-bottom-visual-darwin.png
index 29d38bf7..5b8a5dbf 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/legend-bottom-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/legend-bottom-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/legend-top-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/legend-top-visual-darwin.png
index 5b5dde39..c00979cb 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/legend-top-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/legend-top-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/line-multi-series-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/line-multi-series-visual-darwin.png
index 1bd26ede..c94fcf98 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/line-multi-series-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/line-multi-series-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/long-axis-labels-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/long-axis-labels-visual-darwin.png
index 90c56d95..9ea7dc8c 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/long-axis-labels-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/long-axis-labels-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/map-us-states-categorical-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/map-us-states-categorical-visual-darwin.png
index d9cc2187..91f2b338 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/map-us-states-categorical-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/map-us-states-categorical-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/map-us-states-dark-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/map-us-states-dark-visual-darwin.png
index efae1610..f5976248 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/map-us-states-dark-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/map-us-states-dark-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/map-us-states-light-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/map-us-states-light-visual-darwin.png
index d88fc8bb..09e7f783 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/map-us-states-light-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/map-us-states-light-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/map-world-equal-earth-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/map-world-equal-earth-visual-darwin.png
index 983be66e..eea7c53e 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/map-world-equal-earth-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/map-world-equal-earth-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/multi-series-area-overlap-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/multi-series-area-overlap-visual-darwin.png
index c96d2a37..e0f9f09f 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/multi-series-area-overlap-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/multi-series-area-overlap-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/multi-series-area-stacked-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/multi-series-area-stacked-visual-darwin.png
index f8aeb952..f29e89ae 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/multi-series-area-stacked-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/multi-series-area-stacked-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/parliament-compact-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/parliament-compact-visual-darwin.png
index 1a228fec..113a5208 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/parliament-compact-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/parliament-compact-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/parliament-eu-multi-party-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/parliament-eu-multi-party-visual-darwin.png
index 6cfbe0dd..1b0035d3 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/parliament-eu-multi-party-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/parliament-eu-multi-party-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/parliament-us-house-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/parliament-us-house-visual-darwin.png
index 0125a360..e3b97089 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/parliament-us-house-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/parliament-us-house-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/pie-with-legend-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/pie-with-legend-visual-darwin.png
index 036cc712..e5cd6298 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/pie-with-legend-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/pie-with-legend-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/preset-broadsheet-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/preset-broadsheet-visual-darwin.png
index 7caa63e5..c7ff8416 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/preset-broadsheet-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/preset-broadsheet-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/preset-terminal-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/preset-terminal-visual-darwin.png
index 537c0fb4..4b048949 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/preset-terminal-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/preset-terminal-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/range-arrow-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/range-arrow-visual-darwin.png
index 5800043a..3fc2893e 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/range-arrow-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/range-arrow-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/range-bar-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/range-bar-visual-darwin.png
index ba7e0880..55812d04 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/range-bar-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/range-bar-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/range-dumbbell-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/range-dumbbell-visual-darwin.png
index b873cc20..dba04abd 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/range-dumbbell-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/range-dumbbell-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/rect-heatmap-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/rect-heatmap-visual-darwin.png
index 2e4a94be..0fc7169b 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/rect-heatmap-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/rect-heatmap-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/rotated-labels-with-source-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/rotated-labels-with-source-visual-darwin.png
index e1b42598..017aa804 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/rotated-labels-with-source-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/rotated-labels-with-source-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/sankey-energy-dark-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/sankey-energy-dark-visual-darwin.png
index 7182b55d..ae9a3f62 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/sankey-energy-dark-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/sankey-energy-dark-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/sankey-energy-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/sankey-energy-visual-darwin.png
index 7ffa1c49..1bf5ff7a 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/sankey-energy-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/sankey-energy-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/sankey-narrow-long-labels-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/sankey-narrow-long-labels-visual-darwin.png
index c243f7cc..31fcfe62 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/sankey-narrow-long-labels-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/sankey-narrow-long-labels-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/sankey-other-bucket-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/sankey-other-bucket-visual-darwin.png
index 74ed856e..cc970c71 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/sankey-other-bucket-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/sankey-other-bucket-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/scatter-trendline-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/scatter-trendline-visual-darwin.png
index 1fa96a5b..67c16751 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/scatter-trendline-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/scatter-trendline-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/scrolly-step-annotated-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/scrolly-step-annotated-visual-darwin.png
index 07940c3f..2a178d68 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/scrolly-step-annotated-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/scrolly-step-annotated-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/scrolly-step-base-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/scrolly-step-base-visual-darwin.png
index ce2fc9cf..feb4794f 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/scrolly-step-base-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/scrolly-step-base-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/scrolly-step-highlight-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/scrolly-step-highlight-visual-darwin.png
index d76eefe3..ad921b9e 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/scrolly-step-highlight-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/scrolly-step-highlight-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/scrolly-step-payoff-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/scrolly-step-payoff-visual-darwin.png
index d80809a6..a2c320d6 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/scrolly-step-payoff-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/scrolly-step-payoff-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/scrolly-step-zoomed-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/scrolly-step-zoomed-visual-darwin.png
index 1cbcaeac..863a1f07 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/scrolly-step-zoomed-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/scrolly-step-zoomed-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/series-search-mobile-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/series-search-mobile-visual-darwin.png
index 5ed08ede..5b463573 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/series-search-mobile-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/series-search-mobile-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/series-search-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/series-search-visual-darwin.png
index 4aef0320..8320428e 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/series-search-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/series-search-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/simple-columns-flat-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/simple-columns-flat-visual-darwin.png
index 4f20c9b9..8b82e859 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/simple-columns-flat-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/simple-columns-flat-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/slope-market-share-compact-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/slope-market-share-compact-visual-darwin.png
index 1f74686a..2858fe80 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/slope-market-share-compact-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/slope-market-share-compact-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/slope-market-share-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/slope-market-share-visual-darwin.png
index 81de2123..570e578b 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/slope-market-share-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/slope-market-share-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/stacked-column-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/stacked-column-visual-darwin.png
index a7819fe4..5d5d1e6c 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/stacked-column-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/stacked-column-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/table-basic-regular-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/table-basic-regular-visual-darwin.png
index f4278385..0e54cb1c 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/table-basic-regular-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/table-basic-regular-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/table-cards-mobile-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/table-cards-mobile-visual-darwin.png
index 54281190..5aeca9d7 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/table-cards-mobile-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/table-cards-mobile-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/table-delta-bars-condensed-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/table-delta-bars-condensed-visual-darwin.png
index ba6b737a..cf85be41 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/table-delta-bars-condensed-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/table-delta-bars-condensed-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/table-heatmap-dark-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/table-heatmap-dark-visual-darwin.png
index b0a654f0..691bbaee 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/table-heatmap-dark-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/table-heatmap-dark-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/table-sparklines-shared-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/table-sparklines-shared-visual-darwin.png
index 3571d317..4860feef 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/table-sparklines-shared-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/table-sparklines-shared-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/text-mark-labels-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/text-mark-labels-visual-darwin.png
index c38454a1..b7cf869f 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/text-mark-labels-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/text-mark-labels-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/tilemap-categorical-dark-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/tilemap-categorical-dark-visual-darwin.png
index 707152b8..7a3b21d5 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/tilemap-categorical-dark-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/tilemap-categorical-dark-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/tilemap-quantitative-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/tilemap-quantitative-visual-darwin.png
index eb5156bd..98489ebd 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/tilemap-quantitative-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/tilemap-quantitative-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/us-bubbles-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/us-bubbles-visual-darwin.png
index 324d1a05..80054736 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/us-bubbles-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/us-bubbles-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/v9-crowded-callouts-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/v9-crowded-callouts-visual-darwin.png
new file mode 100644
index 00000000..25ac36e6
Binary files /dev/null and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/v9-crowded-callouts-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/v9-curves-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/v9-curves-visual-darwin.png
new file mode 100644
index 00000000..031cd5a4
Binary files /dev/null and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/v9-curves-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/v9-notes-sizing-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/v9-notes-sizing-visual-darwin.png
new file mode 100644
index 00000000..73d9d951
Binary files /dev/null and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/v9-notes-sizing-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/v9-reference-labels-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/v9-reference-labels-visual-darwin.png
new file mode 100644
index 00000000..b4e0bcd7
Binary files /dev/null and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/v9-reference-labels-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/waffle-basic-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/waffle-basic-visual-darwin.png
index ac6f82bd..09e08402 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/waffle-basic-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/waffle-basic-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/waffle-highlight-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/waffle-highlight-visual-darwin.png
index c2d693a1..61afd2a3 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/waffle-highlight-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/waffle-highlight-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/world-diverging-quantize-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/world-diverging-quantize-visual-darwin.png
index 92bf1806..d9419671 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/world-diverging-quantize-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/world-diverging-quantize-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/you-draw-it-dark-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/you-draw-it-dark-visual-darwin.png
index b7b679d3..f8cf0325 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/you-draw-it-dark-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/you-draw-it-dark-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/you-draw-it-mobile-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/you-draw-it-mobile-visual-darwin.png
index 81272f47..cf016b48 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/you-draw-it-mobile-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/you-draw-it-mobile-visual-darwin.png differ
diff --git a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/you-draw-it-visual-darwin.png b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/you-draw-it-visual-darwin.png
index f1d67bed..806da981 100644
Binary files a/e2e/visual/__screenshots__/stories.spec.ts-snapshots/you-draw-it-visual-darwin.png and b/e2e/visual/__screenshots__/stories.spec.ts-snapshots/you-draw-it-visual-darwin.png differ
diff --git a/e2e/visual/capture.ts b/e2e/visual/capture.ts
index 577006c0..257d7ebc 100644
--- a/e2e/visual/capture.ts
+++ b/e2e/visual/capture.ts
@@ -11,6 +11,24 @@ export async function captureStory(
screenshotName: string,
query = '',
) {
+ // React may replace head styles while a story mounts. Hide only development
+ // overlays at their DOM owner as well, including any remount during capture.
+ await page.addInitScript(() => {
+ const hide = () => {
+ for (const element of document.querySelectorAll(
+ '.ladle-theme-picker, .oc-github-link',
+ )) {
+ if (element.style.getPropertyValue('display') !== 'none')
+ element.style.setProperty('display', 'none', 'important');
+ }
+ };
+ new MutationObserver(hide).observe(document, {
+ childList: true,
+ subtree: true,
+ attributes: true,
+ attributeFilter: ['class', 'style'],
+ });
+ });
await page.goto(`/?mode=preview&story=${encodeURIComponent(slug)}${query}`);
// Kill animations so enter/stagger/annotation-delay timings don't leak
diff --git a/e2e/visual/stories-mobile.spec.ts b/e2e/visual/stories-mobile.spec.ts
index 352db986..98878de3 100644
--- a/e2e/visual/stories-mobile.spec.ts
+++ b/e2e/visual/stories-mobile.spec.ts
@@ -15,6 +15,11 @@ import { captureStory } from './capture';
*/
const stories = [
+ { name: 'v9-reference-labels', slug: 'features--annotation-reference-labels' },
+ { name: 'v9-crowded-callouts', slug: 'features--annotation-crowded-callouts' },
+ { name: 'v9-notes-sizing', slug: 'features--annotation-notes-sizing' },
+ { name: 'v9-curves', slug: 'features--annotation-curves' },
+
{ name: 'mr-long-title', slug: 'testing--mobile-regression--long-title-mobile' },
{ name: 'mr-grouped-columns-labels-all', slug: 'testing--mobile-regression--grouped-columns-labels-all' },
{ name: 'mr-grouped-bars-many-rows', slug: 'testing--mobile-regression--grouped-bars-many-rows' },
@@ -31,6 +36,8 @@ const stories = [
{ name: 'mr-chrome-layout-grow', slug: 'testing--mobile-regression--chrome-layout-grow' },
// maxLines — the same runaway title capped at 2 lines with an ellipsis.
{ name: 'mr-chrome-max-lines', slug: 'testing--mobile-regression--chrome-max-lines' },
+ // Outside reference lanes that cannot fit: labels must stay on canvas.
+ { name: 'mr-reference-lane-overflow', slug: 'testing--mobile-regression--reference-lane-overflow' },
{ name: 'bar-vertical', slug: 'testing--fixtures--simple-columns' },
{ name: 'line-multi-series', slug: 'testing--fixtures--gdp-growth' },
{ name: 'stacked-column', slug: 'testing--fixtures--energy-mix' },
diff --git a/e2e/visual/stories.spec.ts b/e2e/visual/stories.spec.ts
index 8b7cc655..3d4a6115 100644
--- a/e2e/visual/stories.spec.ts
+++ b/e2e/visual/stories.spec.ts
@@ -13,6 +13,11 @@ import { captureStory } from './capture';
* compress the chart area.
*/
const stories: Array<{ name: string; slug: string; query?: string; note?: string }> = [
+ { name: 'v9-reference-labels', slug: 'features--annotation-reference-labels' },
+ { name: 'v9-crowded-callouts', slug: 'features--annotation-crowded-callouts' },
+ { name: 'v9-notes-sizing', slug: 'features--annotation-notes-sizing' },
+ { name: 'v9-curves', slug: 'features--annotation-curves' },
+
{ name: 'bar-vertical', slug: 'testing--fixtures--simple-columns' },
{ name: 'bar-horizontal-gradient', slug: 'testing--fixtures--simple-bars' },
{ name: 'line-multi-series', slug: 'testing--fixtures--gdp-growth' },
diff --git a/examples/src/__tests__/__fixtures__/v8-parity.json.gz b/examples/src/__tests__/__fixtures__/v8-parity.json.gz
new file mode 100644
index 00000000..91d62210
Binary files /dev/null and b/examples/src/__tests__/__fixtures__/v8-parity.json.gz differ
diff --git a/examples/src/__tests__/migration-parity.test.ts b/examples/src/__tests__/migration-parity.test.ts
index 00cf593b..67ee0cfe 100644
--- a/examples/src/__tests__/migration-parity.test.ts
+++ b/examples/src/__tests__/migration-parity.test.ts
@@ -56,20 +56,22 @@ function compile(spec: VizSpec, width: number) {
}
}
-describe('authored example migration parity', () => {
+describe('authored example v8 migration compatibility', () => {
it('discovers examples across the gallery and testing stories', () => {
expect(fixtures.length).toBeGreaterThan(100);
});
for (const { name, spec } of fixtures) {
it(name, () => {
- const { spec: migrated, repairs } = migrateSpecV8(spec);
+ const before = JSON.stringify(spec);
+ const { spec: migrated } = migrateSpecV8(spec);
expect(migrateSpecV8(migrated)).toEqual({ spec: migrated, repairs: [] });
- // The approved end-of-line rewrite deliberately changes placement.
- if (repairs.some((repair) => repair.reason.includes('end-of-line'))) return;
+ expect(JSON.stringify(spec)).toBe(before);
for (const width of [390, 1280]) {
- // Layouts contain formatter/scale closures with fresh identities.
- // Compare all serialized geometry, text, colors and layout metadata.
- expect(JSON.stringify(compile(migrated, width))).toBe(JSON.stringify(compile(spec, width)));
+ // v9 deliberately retunes placement. Migration preserves authored styles,
+ // not pixel geometry; every collected legacy example must still compile.
+ const result = compile(migrated, width);
+ expect(result).toBeDefined();
+ expect(JSON.stringify(result)).toBe(JSON.stringify(compile(migrated, width)));
}
});
}
diff --git a/examples/src/__tests__/migration-v8-approved-changes.ts b/examples/src/__tests__/migration-v8-approved-changes.ts
new file mode 100644
index 00000000..a2ceec94
--- /dev/null
+++ b/examples/src/__tests__/migration-v8-approved-changes.ts
@@ -0,0 +1,9346 @@
+import type { GeometryValue } from './parity-signature';
+
+/** Approved pre-RFC v9 fixes, verified against source commit 93112314.
+ * The released v8 capture stays unchanged. Only these exact before/after
+ * path values are exempted; no fixture or geometry category is exempted. */
+export const APPROVED_PRE_RFC_LAYOUT: Record<
+ string,
+ {
+ reason: string;
+ changes: Record>;
+ }
+> = {
+ '../charts/mobile-regression.stories.tsx.inlineYTitleSpec @390': {
+ reason:
+ '93112314 frame: point-scale x labels reserve their actual flat extent; source numeric domain unchanged.',
+ changes: {
+ area: {
+ 'rect.height': [118.68, 148.65],
+ },
+ chrome: {
+ bottomAnchorY: [314.43, 344.4],
+ },
+ axes: {
+ 'x.end.y': [279.43, 309.4],
+ 'x.start.y': [279.43, 309.4],
+ 'y.end.y': [279.43, 309.4],
+ 'y.tick.0.position': [279.43, 309.4],
+ 'y.tick.1.position': [239.87, 259.85],
+ 'y.tick.2.position': [200.31, 210.3],
+ 'y.titlePosition.y': [220.09, 235.08],
+ },
+ marks: {
+ '0.height': [49.45, 61.94],
+ '0.y': [180.53, 185.52],
+ '1.cy': [196.35, 205.34],
+ '2.cy': [190.42, 197.91],
+ '3.cy': [204.27, 215.26],
+ '4.cy': [186.46, 192.96],
+ '5.cy': [180.53, 185.52],
+ '6.cy': [229.98, 247.46],
+ },
+ },
+ },
+ '../gallery/charts-bar-column.stories.tsx.columnsSpec @390': {
+ reason:
+ 'Pre-RFC v9 mark-anchor correction places annotation connector tails on compiled column bounds.',
+ changes: {
+ annotations: {
+ 'a0.connector.to.y': [241.28, 226.03],
+ },
+ },
+ },
+ '../gallery/charts-bar-column.stories.tsx.columnsSpec @1280': {
+ reason:
+ 'Pre-RFC v9 mark-anchor correction places annotation connector tails on compiled column bounds.',
+ changes: {
+ annotations: {
+ 'a0.connector.to.y': [249.59, 234.34],
+ },
+ },
+ },
+ '../gallery/charts-building-blocks.stories.tsx.directLabelSpec @1280': {
+ reason:
+ 'Pre-RFC v9 shared field precision removes redundant trailing zeroes from numeric axis ticks.',
+ changes: {
+ axes: {
+ 'x.tick.1.label': ['1.50', '1.5'],
+ 'x.tick.3.label': ['2.50', '2.5'],
+ 'x.tick.5.label': ['3.50', '3.5'],
+ },
+ },
+ },
+ '../gallery/charts-building-blocks.stories.tsx.scatterLayer @1280': {
+ reason:
+ 'Pre-RFC v9 shared field precision removes redundant trailing zeroes from numeric axis ticks.',
+ changes: {
+ axes: {
+ 'x.tick.1.label': ['1.50', '1.5'],
+ 'x.tick.3.label': ['2.50', '2.5'],
+ 'x.tick.5.label': ['3.50', '3.5'],
+ },
+ },
+ },
+ '../gallery/charts-building-blocks.stories.tsx.labelLayer @1280': {
+ reason:
+ 'Pre-RFC v9 shared field precision removes redundant trailing zeroes from numeric axis ticks.',
+ changes: {
+ axes: {
+ 'x.tick.1.label': ['1.50', '1.5'],
+ 'x.tick.3.label': ['2.50', '2.5'],
+ 'x.tick.5.label': ['3.50', '3.5'],
+ },
+ },
+ },
+ '../gallery/charts-building-blocks.stories.tsx.composedSpecForPanel @1280': {
+ reason:
+ 'Pre-RFC v9 shared field precision removes redundant trailing zeroes from numeric axis ticks.',
+ changes: {
+ axes: {
+ 'x.tick.1.label': ['1.50', '1.5'],
+ 'x.tick.3.label': ['2.50', '2.5'],
+ 'x.tick.5.label': ['3.50', '3.5'],
+ },
+ },
+ },
+ '../testing/fixtures-misc.stories.tsx.textMarkLabelSpec @1280': {
+ reason:
+ 'Pre-RFC v9 shared field precision removes redundant trailing zeroes from numeric axis ticks.',
+ changes: {
+ axes: {
+ 'x.tick.1.label': ['1.50', '1.5'],
+ 'x.tick.3.label': ['2.50', '2.5'],
+ 'x.tick.5.label': ['3.50', '3.5'],
+ },
+ },
+ },
+ '../gallery/charts-building-blocks.stories.tsx.dualAxisSpec @390': {
+ reason:
+ '93112314 shared figure frame aligns independent-axis marks and y2 ticks to the same plot.',
+ changes: {
+ axes: {
+ 'y2.tick.0.position': [271.97, 283.66],
+ 'y2.tick.1.position': [232.2, 240.24],
+ 'y2.tick.2.position': [192.42, 196.81],
+ 'y2.tick.3.position': [152.65, 153.39],
+ },
+ marks: {
+ '11.height': [96.24, 105.09],
+ '11.y': [173.33, 175.97],
+ '12.cy': [182.88, 186.39],
+ '13.cy': [173.33, 175.97],
+ '14.cy': [180.49, 183.78],
+ '15.cy': [190.04, 194.2],
+ '16.cy': [201.17, 206.36],
+ '17.cy': [215.49, 222],
+ '18.cy': [229.01, 236.76],
+ '19.cy': [238.56, 247.18],
+ '20.cy': [249.69, 259.34],
+ '21.cy': [259.24, 269.76],
+ '22.cy': [269.58, 281.05],
+ },
+ },
+ },
+ '../testing/fixtures-dual-axis.stories.tsx.dualAxisSpec @390': {
+ reason:
+ '93112314 shared figure frame aligns independent-axis marks and y2 ticks to the same plot.',
+ changes: {
+ axes: {
+ 'y2.tick.0.position': [271.97, 283.66],
+ 'y2.tick.1.position': [232.2, 240.24],
+ 'y2.tick.2.position': [192.42, 196.81],
+ 'y2.tick.3.position': [152.65, 153.39],
+ },
+ marks: {
+ '11.height': [96.24, 105.09],
+ '11.y': [173.33, 175.97],
+ '12.cy': [182.88, 186.39],
+ '13.cy': [173.33, 175.97],
+ '14.cy': [180.49, 183.78],
+ '15.cy': [190.04, 194.2],
+ '16.cy': [201.17, 206.36],
+ '17.cy': [215.49, 222],
+ '18.cy': [229.01, 236.76],
+ '19.cy': [238.56, 247.18],
+ '20.cy': [249.69, 259.34],
+ '21.cy': [259.24, 269.76],
+ '22.cy': [269.58, 281.05],
+ },
+ },
+ },
+ '../gallery/charts-building-blocks.stories.tsx.spanSpec @390': {
+ reason: '93112314 frame: point-scale x labels reserve their actual flat extent.',
+ changes: {
+ area: {
+ 'rect.height': [143.68, 158.3],
+ },
+ chrome: {
+ bottomAnchorY: [297.38, 312],
+ },
+ axes: {
+ 'x.end.y': [271.38, 286],
+ 'x.start.y': [271.38, 286],
+ 'y.end.y': [271.38, 286],
+ 'y.tick.0.position': [271.38, 286],
+ 'y.tick.1.position': [241.45, 253.02],
+ 'y.tick.2.position': [211.51, 220.04],
+ 'y.tick.3.position': [181.58, 187.06],
+ 'y.tick.4.position': [151.65, 154.08],
+ 'y.titlePosition.y': [199.54, 206.85],
+ },
+ marks: {
+ '0.y1': [250.43, 262.91],
+ '0.y2': [215.71, 224.66],
+ '1.y1': [265.99, 280.06],
+ '1.y2': [216.3, 225.32],
+ '10.y1': [244.44, 256.32],
+ '10.y2': [200.74, 208.17],
+ '11.y1': [246.24, 258.3],
+ '11.y2': [212.71, 221.36],
+ '2.y1': [252.22, 264.89],
+ '2.y2': [206.73, 214.77],
+ '3.y1': [215.11, 224],
+ '3.y2': [157.63, 160.68],
+ '4.y1': [199.54, 206.85],
+ '4.y2': [139.07, 140.23],
+ '5.y1': [170.8, 175.19],
+ '5.y2': [119.32, 118.47],
+ '6.y1': [139.07, 140.23],
+ '6.y2': [90.58, 86.81],
+ '7.y1': [150.45, 152.76],
+ '7.y2': [106.15, 103.95],
+ '8.y1': [167.81, 171.89],
+ '8.y2': [125.9, 125.72],
+ '9.y1': [197.15, 204.21],
+ '9.y2': [155.84, 158.7],
+ },
+ },
+ },
+ '../gallery/dashboards.layouts.tsx.funnelConversionSpec @390': {
+ reason: '93112314 frame: point-scale x labels reserve their actual flat extent.',
+ changes: {
+ area: {
+ 'rect.height': [265.93, 280.55],
+ },
+ chrome: {
+ bottomAnchorY: [369.38, 384],
+ },
+ axes: {
+ 'x.end.y': [343.38, 358],
+ 'x.start.y': [343.38, 358],
+ 'y.end.y': [343.38, 358],
+ 'y.tick.0.position': [318.06, 331.28],
+ 'y.tick.1.position': [254.74, 264.48],
+ 'y.tick.2.position': [191.42, 197.69],
+ 'y.tick.3.position': [128.1, 130.89],
+ },
+ marks: {
+ '0.height': [22.79, 24.05],
+ '0.y': [319.32, 332.62],
+ '1.height': [70.92, 74.81],
+ '1.y': [86.31, 86.8],
+ },
+ },
+ },
+ '../gallery/features-animation.stories.tsx.annotationDelaySpec @390': {
+ reason: 'Pre-RFC approved UTC scale for date-only fields fixes tick identity and position.',
+ changes: {
+ axes: {
+ 'x.tick.0.label': ['2024', 'Jul 2023'],
+ 'x.tick.0.position': [170.74, 58],
+ 'x.tick.1.label': [undefined, 'Apr 2024'],
+ 'x.tick.1.position': [undefined, 226.5],
+ 'x.tick.2.label': [undefined, 'Oct 2024'],
+ 'x.tick.2.position': [undefined, 338.62],
+ },
+ },
+ },
+ '../testing/fixtures-bars.stories.tsx.simpleColumnSpec @390': {
+ reason:
+ 'Pre-RFC v9 mark-anchor correction places annotation connector tails on compiled column bounds.',
+ changes: {
+ annotations: {
+ 'a0.connector.to.x': [302.35, 297.3],
+ 'a0.connector.to.y': [242.27, 233.29],
+ },
+ },
+ },
+ '../testing/fixtures-bars.stories.tsx.simpleColumnSpec @1280': {
+ reason:
+ 'Pre-RFC v9 mark-anchor correction places annotation connector tails on compiled column bounds.',
+ changes: {
+ annotations: {
+ 'a0.connector.to.x': [1000.47, 994.45],
+ 'a0.connector.to.y': [250.12, 234.64],
+ },
+ },
+ },
+ '../testing/fixtures-bars.stories.tsx.simpleBarSpec @390': {
+ reason:
+ 'Pre-RFC v9 mark-anchor correction places annotation connector tails on compiled horizontal bar bounds.',
+ changes: {
+ annotations: {
+ 'a0.connector.to.x': [368.85, 353.04],
+ 'a0.connector.to.y': [209.99, 203.38],
+ 'a1.connector.to.x': [363.6, 345.93],
+ 'a1.connector.to.y': [204.53, 198.95],
+ },
+ },
+ },
+ '../testing/fixtures-bars.stories.tsx.simpleBarSpec @1280': {
+ reason:
+ 'Pre-RFC v9 mark-anchor correction places annotation connector tails on compiled horizontal bar bounds.',
+ changes: {
+ annotations: {
+ 'a0.connector.to.x': [1251.32, 1246.65],
+ 'a0.connector.to.y': [199.12, 194.79],
+ 'a1.connector.to.x': [1225.31, 1207.65],
+ 'a1.connector.to.y': [189.86, 184.24],
+ },
+ },
+ },
+ '../testing/fixtures-bars.stories.tsx.tempSpec @390': {
+ reason:
+ 'Pre-RFC v9 shared field precision changes tick strings/gutters; existing D14 annotation placement remains separately allowlisted.',
+ changes: {
+ area: {
+ 'rect.width': [317.76, 323.88],
+ 'rect.x': [58.24, 52.12],
+ },
+ axes: {
+ 'x.start.x': [58.24, 52.12],
+ 'x.tick.0.position': [72.17, 66.33],
+ 'x.tick.1.position': [94.47, 89.05],
+ 'x.tick.10.position': [295.17, 293.61],
+ 'x.tick.11.position': [317.46, 316.34],
+ 'x.tick.12.position': [339.76, 339.07],
+ 'x.tick.13.position': [362.06, 361.79],
+ 'x.tick.2.position': [116.77, 111.78],
+ 'x.tick.3.position': [139.07, 134.51],
+ 'x.tick.4.position': [161.37, 157.24],
+ 'x.tick.5.position': [183.67, 179.97],
+ 'x.tick.6.position': [205.97, 202.7],
+ 'x.tick.7.position': [228.27, 225.42],
+ 'x.tick.8.position': [250.57, 248.15],
+ 'x.tick.9.position': [272.87, 270.88],
+ 'y.end.x': [58.24, 52.12],
+ 'y.start.x': [58.24, 52.12],
+ 'y.tick.0.label': ['\u22120.50', '\u22120.5'],
+ 'y.tick.2.label': ['0.50', '0.5'],
+ },
+ marks: {
+ '0.width': [16.72, 17.05],
+ '0.x': [63.81, 57.8],
+ '1.width': [16.72, 17.05],
+ '1.x': [86.11, 80.53],
+ '10.width': [16.72, 17.05],
+ '10.x': [286.8, 285.09],
+ '11.width': [16.72, 17.05],
+ '11.x': [309.1, 307.81],
+ '12.width': [16.72, 17.05],
+ '12.x': [331.4, 330.54],
+ '13.width': [16.72, 17.05],
+ '13.x': [353.7, 353.27],
+ '2.width': [16.72, 17.05],
+ '2.x': [108.41, 103.26],
+ '3.width': [16.72, 17.05],
+ '3.x': [130.71, 125.99],
+ '4.width': [16.72, 17.05],
+ '4.x': [153.01, 148.72],
+ '5.width': [16.72, 17.05],
+ '5.x': [175.31, 171.44],
+ '6.width': [16.72, 17.05],
+ '6.x': [197.61, 194.17],
+ '7.width': [16.72, 17.05],
+ '7.x': [219.91, 216.9],
+ '8.width': [16.72, 17.05],
+ '8.x': [242.2, 239.63],
+ '9.width': [16.72, 17.05],
+ '9.x': [264.5, 262.36],
+ },
+ annotations: {
+ 'a0.connector.from.x': [156.27, 150.85],
+ 'a0.connector.to.x': [102.22, 96.8],
+ 'a0.dot.x': [94.47, 89.05],
+ 'a0.label.x': [164.27, 158.85],
+ 'a1.connector.from.x': [162.04, 161.77],
+ 'a1.connector.to.x': [354.34, 354.07],
+ 'a1.dot.x': [362.06, 361.79],
+ 'a1.label.x': [154.06, 153.79],
+ 'a2.line.start.x': [58.24, 52.12],
+ },
+ legend: {
+ 'legends.0.bounds.x': [58.24, 52.12],
+ 'legends.0.entry.0.x': [58.24, 52.12],
+ 'legends.0.entry.1.x': [118.63, 112.52],
+ },
+ },
+ },
+ '../testing/fixtures-bars.stories.tsx.tempSpec @1280': {
+ reason:
+ 'Pre-RFC v9 shared field precision changes tick strings/gutters; existing D14 annotation placement remains separately allowlisted.',
+ changes: {
+ axes: {
+ 'y.tick.0.label': ['\u22120.50', '\u22120.5'],
+ 'y.tick.2.label': ['0.50', '0.5'],
+ },
+ annotations: {
+ 'a0.connector.from.x': [291.2, 208.02],
+ 'a0.connector.from.y': [197.88, 180.6],
+ 'a0.connector.to.x': [214.85, 208.02],
+ 'a0.connector.to.y': [238.74, 234.65],
+ 'a0.label.x': [257.37, 133.59],
+ 'a0.label.y': [172.24, 151.8],
+ },
+ },
+ },
+ '../testing/fixtures-refresh.stories.tsx.flatColumnsSpec @1280': {
+ reason: 'Pre-RFC v9 shared field precision changes tick strings and measured y-label gutter.',
+ changes: {
+ axes: {
+ 'y.tick.1.label': ['0.50', '0.5'],
+ 'y.tick.3.label': ['1.50', '1.5'],
+ },
+ },
+ },
+ '../testing/fixtures-misc.stories.tsx.rotatedSpec @1280': {
+ reason:
+ '93112314 frame reserves actual rotated categorical labels, keeping footer within the fixed figure.',
+ changes: {
+ area: {
+ 'rect.height': [229.2, 156.86],
+ },
+ chrome: {
+ bottomAnchorY: [414.74, 342.4],
+ },
+ axes: {
+ 'x.end.y': [316.4, 244.06],
+ 'x.start.y': [316.4, 244.06],
+ 'y.end.y': [316.4, 244.06],
+ 'y.gridlines': [8, 4],
+ 'y.tick.0.position': [316.4, 244.06],
+ 'y.tick.1.label': ['5', '10'],
+ 'y.tick.1.position': [283.66, 199.24],
+ 'y.tick.2.label': ['10', '20'],
+ 'y.tick.2.position': [250.91, 154.42],
+ 'y.tick.3.label': ['15', '30'],
+ 'y.tick.3.position': [218.17, 109.61],
+ 'y.tick.4.label': ['20', undefined],
+ 'y.tick.4.position': [185.43, undefined],
+ 'y.tick.5.label': ['25', undefined],
+ 'y.tick.5.position': [152.69, undefined],
+ 'y.tick.6.label': ['30', undefined],
+ 'y.tick.6.position': [119.94, undefined],
+ 'y.tick.7.label': ['35', undefined],
+ 'y.tick.7.position': [87.2, undefined],
+ 'y.titlePosition.y': [201.8, 165.63],
+ },
+ marks: {
+ '0.height': [212.17, 145.2],
+ '0.y': [104.23, 98.85],
+ '1.height': [85.79, 58.71],
+ '1.y': [230.61, 185.35],
+ '2.height': [83.82, 57.36],
+ '2.y': [232.58, 186.69],
+ '3.height': [68.76, 47.06],
+ '3.y': [247.64, 197],
+ '4.height': [58.28, 39.89],
+ '4.y': [258.12, 204.17],
+ '5.height': [55.01, 37.65],
+ '5.y': [261.39, 206.41],
+ },
+ labels: {
+ '0.label.y': [92.23, 86.85],
+ '1.label.y': [218.61, 173.35],
+ '2.label.y': [220.58, 174.69],
+ '3.label.y': [235.64, 185],
+ '4.label.y': [246.12, 192.17],
+ '5.label.y': [249.39, 194.41],
+ },
+ },
+ },
+ '../testing/fixtures-refresh.stories.tsx.flatColumnsSpec @390': {
+ reason: 'Pre-RFC v9 shared field precision changes tick strings and measured y-label gutter.',
+ changes: {
+ area: {
+ 'rect.width': [323.59, 326],
+ 'rect.x': [52.41, 50],
+ },
+ axes: {
+ 'x.start.x': [52.41, 50],
+ 'x.tick.0.position': [76.92, 74.7],
+ 'x.tick.1.position': [116.14, 114.21],
+ 'x.tick.2.position': [155.37, 153.73],
+ 'x.tick.3.position': [194.59, 193.24],
+ 'x.tick.4.position': [233.81, 232.76],
+ 'x.tick.5.position': [273.04, 272.27],
+ 'x.tick.6.position': [312.26, 311.79],
+ 'x.tick.7.position': [351.49, 351.3],
+ 'y.end.x': [52.41, 50],
+ 'y.start.x': [52.41, 50],
+ 'y.tick.1.label': ['0.50', '0.5'],
+ 'y.tick.3.label': ['1.50', '1.5'],
+ },
+ marks: {
+ '0.width': [29.42, 29.64],
+ '0.x': [62.21, 59.88],
+ '1.width': [29.42, 29.64],
+ '1.x': [101.44, 99.39],
+ '2.width': [29.42, 29.64],
+ '2.x': [140.66, 138.91],
+ '3.width': [29.42, 29.64],
+ '3.x': [179.88, 178.42],
+ '4.width': [29.42, 29.64],
+ '4.x': [219.11, 217.94],
+ '5.width': [29.42, 29.64],
+ '5.x': [258.33, 257.45],
+ '6.width': [29.42, 29.64],
+ '6.x': [297.55, 296.97],
+ '7.width': [29.42, 29.64],
+ '7.x': [336.78, 336.48],
+ },
+ labels: {
+ '0.label.x': [76.92, 74.7],
+ '1.label.x': [116.14, 114.21],
+ '2.label.x': [155.37, 153.73],
+ '3.label.x': [194.59, 193.24],
+ '4.label.x': [233.81, 232.76],
+ '5.label.x': [273.04, 272.27],
+ '6.label.x': [312.26, 311.79],
+ '7.label.x': [351.49, 351.3],
+ },
+ },
+ },
+};
+
+/** B1 endpoint correctness: reserve the 18px painted gap (formerly 16px),
+ * measure every displayed string and apply shared precision without losing
+ * compact magnitude formatting. Exact released-v8/current values only. */
+export const APPROVED_RFC_B1_LAYOUT: typeof APPROVED_PRE_RFC_LAYOUT = {
+ '../charts/mobile-regression.stories.tsx.autoHeightChromeGrowthSpec @1280': {
+ reason:
+ 'B1 endpoint full-set width, shared precision and corrected 18px reservation of the already-painted gap; compared against released v8 and pre-RFC93112314 source.',
+ changes: {
+ area: {
+ 'rect.width': [1094.18, 1092.18],
+ },
+ axes: {
+ 'x.end.x': [1138.18, 1136.18],
+ 'x.tick.1.position': [284.44, 284.04],
+ 'x.tick.2.position': [497.87, 497.07],
+ 'x.tick.3.position': [711.31, 710.11],
+ 'x.tick.4.position': [924.74, 923.14],
+ 'x.tick.5.position': [1138.18, 1136.18],
+ },
+ marks: {
+ '0.width': [1067.18, 1065.18],
+ '10.cx': [497.87, 497.07],
+ '11.cx': [711.31, 710.11],
+ '12.cx': [924.74, 923.14],
+ '13.cx': [1138.18, 1136.18],
+ '14.width': [1067.18, 1065.18],
+ '16.cx': [284.44, 284.04],
+ '17.cx': [497.87, 497.07],
+ '18.cx': [711.31, 710.11],
+ '19.cx': [924.74, 923.14],
+ '2.cx': [284.44, 284.04],
+ '20.cx': [1138.18, 1136.18],
+ '3.cx': [497.87, 497.07],
+ '4.cx': [711.31, 710.11],
+ '5.cx': [924.74, 923.14],
+ '6.cx': [1138.18, 1136.18],
+ '7.width': [1067.18, 1065.18],
+ '9.cx': [284.44, 284.04],
+ },
+ endpointLabels: {
+ 'bounds.x': [1156.18, 1154.18],
+ },
+ },
+ },
+ '../gallery/charts-line-area.stories.tsx.multiSeriesSpec @1280': {
+ reason:
+ 'B1 endpoint full-set width, shared precision and corrected 18px reservation of the already-painted gap; compared against released v8 and pre-RFC93112314 source.',
+ changes: {
+ area: {
+ 'rect.width': [1085.06, 1083.06],
+ },
+ axes: {
+ 'x.end.x': [1129.06, 1127.06],
+ 'x.tick.1.position': [419.52, 418.86],
+ 'x.tick.2.position': [774.53, 773.2],
+ 'x.tick.3.position': [1129.06, 1127.06],
+ },
+ marks: {
+ '0.width': [1064.06, 1062.06],
+ '1.width': [1064.06, 1062.06],
+ '2.width': [1064.06, 1062.06],
+ },
+ annotations: {
+ 'a0.line.end.x': [1129.06, 1127.06],
+ 'a1.label.x': [426.81, 426.13],
+ 'a1.rect.width': [133.07, 132.82],
+ 'a1.rect.x': [360.28, 359.72],
+ },
+ endpointLabels: {
+ 'bounds.x': [1147.06, 1145.06],
+ },
+ },
+ },
+ '../gallery/charts-line-area.stories.tsx.interactiveSpec @1280': {
+ reason:
+ 'B1 endpoint full-set width, shared precision and corrected 18px reservation of the already-painted gap; compared against released v8 and pre-RFC93112314 source.',
+ changes: {
+ area: {
+ 'rect.width': [1111.64, 1109.64],
+ },
+ axes: {
+ 'x.end.x': [1155.64, 1153.64],
+ 'x.tick.1.position': [287.81, 287.41],
+ 'x.tick.2.position': [505.21, 504.41],
+ 'x.tick.3.position': [722.02, 720.82],
+ 'x.tick.4.position': [938.83, 937.23],
+ 'x.tick.5.position': [1155.64, 1153.64],
+ },
+ marks: {
+ '0.width': [1084.64, 1082.64],
+ '1.width': [1084.64, 1082.64],
+ '2.width': [1084.64, 1082.64],
+ '3.width': [1084.64, 1082.64],
+ '4.width': [1084.64, 1082.64],
+ },
+ endpointLabels: {
+ 'bounds.x': [1173.64, 1171.64],
+ },
+ },
+ },
+ '../gallery/dashboards.layouts.tsx.marketsCompareSpec @1280': {
+ reason:
+ 'B1 endpoint full-set width, shared precision and corrected 18px reservation of the already-painted gap; compared against released v8 and pre-RFC93112314 source.',
+ changes: {
+ area: {
+ 'rect.width': [1088.1, 1086.1],
+ },
+ axes: {
+ 'x.end.x': [1132.1, 1130.1],
+ 'x.tick.1.position': [353.91, 353.38],
+ 'x.tick.2.position': [636.82, 635.75],
+ 'x.tick.3.position': [920.5, 918.9],
+ },
+ marks: {
+ '0.width': [1061.1, 1059.1],
+ '1.width': [1061.1, 1059.1],
+ '2.width': [1061.1, 1059.1],
+ '3.width': [1061.1, 1059.1],
+ },
+ annotations: {
+ 'a0.label.x': [1128.1, 1126.1],
+ 'a0.line.end.x': [1132.1, 1130.1],
+ },
+ endpointLabels: {
+ 'bounds.x': [1150.1, 1148.1],
+ },
+ },
+ },
+ '../gallery/dashboards.stories.tsx.crosshairSpec @1280': {
+ reason:
+ 'B1 endpoint full-set width, shared precision and corrected 18px reservation of the already-painted gap; compared against released v8 and pre-RFC93112314 source.',
+ changes: {
+ area: {
+ 'rect.width': [1088.1, 1086.1],
+ },
+ axes: {
+ 'x.end.x': [1132.1, 1130.1],
+ 'x.tick.1.position': [353.91, 353.38],
+ 'x.tick.2.position': [636.82, 635.75],
+ 'x.tick.3.position': [920.5, 918.9],
+ },
+ marks: {
+ '0.width': [1061.1, 1059.1],
+ '1.width': [1061.1, 1059.1],
+ '2.width': [1061.1, 1059.1],
+ '3.width': [1061.1, 1059.1],
+ },
+ annotations: {
+ 'a0.label.x': [1128.1, 1126.1],
+ 'a0.line.end.x': [1132.1, 1130.1],
+ },
+ endpointLabels: {
+ 'bounds.x': [1150.1, 1148.1],
+ },
+ },
+ },
+ '../gallery/dashboards.layouts.tsx.funnelConversionSpec @1280': {
+ reason:
+ 'B1 endpoint full-set width, shared precision and corrected 18px reservation of the already-painted gap; compared against released v8 and pre-RFC93112314 source.',
+ changes: {
+ area: {
+ 'rect.width': [1103.37, 1101.37],
+ },
+ axes: {
+ 'x.end.x': [1126.37, 1124.37],
+ 'x.tick.1.position': [346.46, 345.92],
+ 'x.tick.2.position': [638.93, 637.84],
+ 'x.tick.3.position': [931.39, 929.76],
+ },
+ marks: {
+ '0.width': [1072.37, 1070.37],
+ '1.width': [1072.37, 1070.37],
+ },
+ endpointLabels: {
+ 'bounds.x': [1144.37, 1142.37],
+ },
+ },
+ },
+ '../gallery/dashboards.layouts.tsx.incidentTtrcSpec @1280': {
+ reason:
+ 'B1 endpoint full-set width, shared precision and corrected 18px reservation of the already-painted gap; compared against released v8 and pre-RFC93112314 source.',
+ changes: {
+ area: {
+ 'rect.width': [1113.89, 1111.89],
+ },
+ axes: {
+ 'x.end.x': [1157.89, 1155.89],
+ 'x.tick.1.position': [307.86, 307.42],
+ 'x.tick.2.position': [550.73, 549.84],
+ 'x.tick.3.position': [793.59, 792.26],
+ 'x.tick.4.position': [1036.46, 1034.68],
+ },
+ marks: {
+ '0.width': [1092.89, 1090.89],
+ '1.width': [1092.89, 1090.89],
+ },
+ endpointLabels: {
+ 'bounds.x': [1175.89, 1173.89],
+ },
+ },
+ },
+ '../gallery/features-animation.stories.tsx.lineDrawSpec @1280': {
+ reason:
+ 'B1 endpoint full-set width, shared precision and corrected 18px reservation of the already-painted gap; compared against released v8 and pre-RFC93112314 source.',
+ changes: {
+ area: {
+ 'rect.width': [1107.06, 1105.06],
+ },
+ axes: {
+ 'x.end.x': [1151.06, 1149.06],
+ 'x.tick.1.position': [364.39, 363.85],
+ 'x.tick.2.position': [657.78, 656.69],
+ 'x.tick.3.position': [954.39, 952.76],
+ },
+ marks: {
+ '0.width': [1080.06, 1078.06],
+ '10.cx': [954.39, 952.76],
+ '11.cx': [1054.34, 1052.52],
+ '12.cx': [1151.06, 1149.06],
+ '13.width': [1080.06, 1078.06],
+ '15.cx': [170.95, 170.76],
+ '16.cx': [264.44, 264.09],
+ '17.cx': [364.39, 363.85],
+ '18.cx': [461.11, 460.39],
+ '19.cx': [561.06, 560.15],
+ '2.cx': [170.95, 170.76],
+ '20.cx': [657.78, 656.69],
+ '21.cx': [757.73, 756.45],
+ '22.cx': [857.67, 856.22],
+ '23.cx': [954.39, 952.76],
+ '24.cx': [1054.34, 1052.52],
+ '25.cx': [1151.06, 1149.06],
+ '3.cx': [264.44, 264.09],
+ '4.cx': [364.39, 363.85],
+ '5.cx': [461.11, 460.39],
+ '6.cx': [561.06, 560.15],
+ '7.cx': [657.78, 656.69],
+ '8.cx': [757.73, 756.45],
+ '9.cx': [857.67, 856.22],
+ },
+ endpointLabels: {
+ 'bounds.x': [1169.06, 1167.06],
+ },
+ },
+ },
+ '../gallery/features-responsive.stories.tsx.flagshipSpec @1280': {
+ reason:
+ 'B1 endpoint full-set width, shared precision and corrected 18px reservation of the already-painted gap; compared against released v8 and pre-RFC93112314 source.',
+ changes: {
+ area: {
+ 'rect.width': [1085.06, 1083.06],
+ },
+ axes: {
+ 'x.end.x': [1129.06, 1127.06],
+ 'x.tick.1.position': [241.43, 241.1],
+ 'x.tick.2.position': [418.86, 418.19],
+ 'x.tick.3.position': [596.77, 595.77],
+ 'x.tick.4.position': [774.2, 772.87],
+ 'x.tick.5.position': [951.63, 949.96],
+ 'x.tick.6.position': [1129.06, 1127.06],
+ },
+ marks: {
+ '0.width': [1065.06, 1063.06],
+ '1.width': [1065.06, 1063.06],
+ '2.width': [1065.06, 1063.06],
+ },
+ endpointLabels: {
+ 'bounds.x': [1147.06, 1145.06],
+ },
+ },
+ },
+ '../gallery/features-responsive.stories.tsx.ratioSpec @1280': {
+ reason:
+ 'B1 endpoint full-set width, shared precision and corrected 18px reservation of the already-painted gap; compared against released v8 and pre-RFC93112314 source.',
+ changes: {
+ area: {
+ 'rect.width': [1106.06, 1104.06],
+ },
+ axes: {
+ 'x.end.x': [1129.06, 1127.06],
+ 'x.tick.1.position': [223.93, 223.59],
+ 'x.tick.2.position': [404.85, 404.19],
+ 'x.tick.3.position': [586.28, 585.28],
+ 'x.tick.4.position': [767.2, 765.87],
+ 'x.tick.5.position': [948.13, 946.47],
+ 'x.tick.6.position': [1129.06, 1127.06],
+ },
+ marks: {
+ '0.width': [1086.06, 1084.06],
+ '1.width': [1086.06, 1084.06],
+ '2.width': [1086.06, 1084.06],
+ },
+ endpointLabels: {
+ 'bounds.x': [1147.06, 1145.06],
+ },
+ },
+ },
+ '../gallery/showcase.stories.tsx.multiLineSpec @1280': {
+ reason:
+ 'B1 endpoint full-set width, shared precision and corrected 18px reservation of the already-painted gap; compared against released v8 and pre-RFC93112314 source.',
+ changes: {
+ area: {
+ 'rect.width': [1111.64, 1109.64],
+ },
+ axes: {
+ 'x.end.x': [1155.64, 1153.64],
+ 'x.tick.1.position': [287.81, 287.41],
+ 'x.tick.2.position': [505.21, 504.41],
+ 'x.tick.3.position': [722.02, 720.82],
+ 'x.tick.4.position': [938.83, 937.23],
+ 'x.tick.5.position': [1155.64, 1153.64],
+ },
+ marks: {
+ '0.width': [1084.64, 1082.64],
+ '1.width': [1084.64, 1082.64],
+ '2.width': [1084.64, 1082.64],
+ '3.width': [1084.64, 1082.64],
+ '4.width': [1084.64, 1082.64],
+ },
+ annotations: {
+ 'a0.dot.x': [505.21, 504.41],
+ 'a0.label.x': [517.21, 516.41],
+ },
+ endpointLabels: {
+ 'bounds.x': [1173.64, 1171.64],
+ },
+ },
+ },
+ '../testing/fixtures-lines.stories.tsx.gdpLineSpec @1280': {
+ reason:
+ 'B1 endpoint full-set width, shared precision and corrected 18px reservation of the already-painted gap; compared against released v8 and pre-RFC93112314 source.',
+ changes: {
+ area: {
+ 'rect.width': [1085.06, 1083.06],
+ },
+ axes: {
+ 'x.end.x': [1129.06, 1127.06],
+ 'x.tick.1.position': [241.43, 241.1],
+ 'x.tick.2.position': [418.86, 418.19],
+ 'x.tick.3.position': [596.77, 595.77],
+ 'x.tick.4.position': [774.2, 772.87],
+ 'x.tick.5.position': [951.63, 949.96],
+ 'x.tick.6.position': [1129.06, 1127.06],
+ },
+ marks: {
+ '0.width': [1065.06, 1063.06],
+ '1.width': [1065.06, 1063.06],
+ '2.width': [1065.06, 1063.06],
+ },
+ annotations: {
+ 'a0.label.x': [500.28, 499.46],
+ 'a0.rect.width': [162.85, 162.54],
+ 'a0.rect.x': [418.86, 418.19],
+ 'a1.line.end.x': [1129.06, 1127.06],
+ },
+ endpointLabels: {
+ 'bounds.x': [1147.06, 1145.06],
+ },
+ },
+ },
+ '../testing/fixtures-lines.stories.tsx.compactGdpSpec @1280': {
+ reason:
+ 'B1 endpoint full-set width, shared precision and corrected 18px reservation of the already-painted gap; compared against released v8 and pre-RFC93112314 source.',
+ changes: {
+ area: {
+ 'rect.width': [1085.06, 1083.06],
+ },
+ axes: {
+ 'x.end.x': [1129.06, 1127.06],
+ 'x.tick.1.position': [241.43, 241.1],
+ 'x.tick.2.position': [418.86, 418.19],
+ 'x.tick.3.position': [596.77, 595.77],
+ 'x.tick.4.position': [774.2, 772.87],
+ 'x.tick.5.position': [951.63, 949.96],
+ 'x.tick.6.position': [1129.06, 1127.06],
+ },
+ marks: {
+ '0.width': [1065.06, 1063.06],
+ '1.width': [1065.06, 1063.06],
+ '2.width': [1065.06, 1063.06],
+ },
+ annotations: {
+ 'a0.label.x': [500.28, 499.46],
+ 'a0.rect.width': [162.85, 162.54],
+ 'a0.rect.x': [418.86, 418.19],
+ 'a1.line.end.x': [1129.06, 1127.06],
+ },
+ endpointLabels: {
+ 'bounds.x': [1147.06, 1145.06],
+ },
+ },
+ },
+ '../testing/fixtures-lines.stories.tsx.fiveSeriesSpec @1280': {
+ reason:
+ 'B1 endpoint full-set width, shared precision and corrected 18px reservation of the already-painted gap; compared against released v8 and pre-RFC93112314 source.',
+ changes: {
+ area: {
+ 'rect.width': [1111.64, 1109.64],
+ },
+ axes: {
+ 'x.end.x': [1155.64, 1153.64],
+ 'x.tick.1.position': [259.52, 259.17],
+ 'x.tick.2.position': [448.56, 447.86],
+ 'x.tick.3.position': [637.08, 636.03],
+ 'x.tick.4.position': [825.6, 824.21],
+ 'x.tick.5.position': [1014.12, 1012.38],
+ },
+ marks: {
+ '0.width': [1084.64, 1082.64],
+ '1.width': [1084.64, 1082.64],
+ '2.width': [1084.64, 1082.64],
+ '3.width': [1084.64, 1082.64],
+ '4.width': [1084.64, 1082.64],
+ },
+ annotations: {
+ 'a0.connector.from.x': [290.97, 290.36],
+ 'a0.connector.to.x': [393.38, 392.77],
+ 'a0.dot.x': [401.04, 400.43],
+ 'a0.label.x': [283.04, 282.43],
+ },
+ endpointLabels: {
+ 'bounds.x': [1173.64, 1171.64],
+ },
+ },
+ },
+ '../testing/fixtures-lines.stories.tsx.multiSeriesAreaOverlapSpec @1280': {
+ reason:
+ 'B1 endpoint full-set width, shared precision and corrected 18px reservation of the already-painted gap; compared against released v8 and pre-RFC93112314 source.',
+ changes: {
+ area: {
+ 'rect.width': [1138.31, 1136.31],
+ },
+ axes: {
+ 'x.end.x': [1161.31, 1159.31],
+ 'x.tick.1.position': [331.4, 330.9],
+ 'x.tick.2.position': [608.04, 607.04],
+ 'x.tick.3.position': [884.67, 883.17],
+ 'x.tick.4.position': [1161.31, 1159.31],
+ },
+ marks: {
+ '0.width': [1107.31, 1105.31],
+ '1.width': [1107.31, 1105.31],
+ '2.width': [1107.31, 1105.31],
+ '3.width': [1107.31, 1105.31],
+ '4.width': [1107.31, 1105.31],
+ '5.width': [1107.31, 1105.31],
+ '6.width': [1107.31, 1105.31],
+ '7.width': [1107.31, 1105.31],
+ },
+ annotations: {
+ 'a0.connector.from.x': [1021.86, 1020.11],
+ 'a0.connector.to.x': [1021.86, 1020.11],
+ 'a0.dot.x': [1021.86, 1020.11],
+ 'a0.label.x': [950.14, 948.39],
+ 'a0.subtitle.x': [950.14, 948.39],
+ },
+ endpointLabels: {
+ 'bounds.x': [1179.31, 1177.31],
+ },
+ },
+ },
+ '../testing/fixtures-lines.stories.tsx.multiSeriesAreaStackedSpec @390': {
+ reason:
+ 'B1 endpoint full-set width, shared precision and corrected 18px reservation of the already-painted gap; compared against released v8 and pre-RFC93112314 source.',
+ changes: {
+ area: {
+ 'rect.width': [280.35, 273.98],
+ },
+ axes: {
+ 'x.end.x': [294.35, 287.98],
+ 'x.tick.1.position': [183.11, 179.65],
+ },
+ marks: {
+ '0.width': [244.35, 237.98],
+ '1.width': [244.35, 237.98],
+ '2.width': [244.35, 237.98],
+ '3.width': [244.35, 237.98],
+ '4.width': [244.35, 237.98],
+ '5.width': [244.35, 237.98],
+ },
+ endpointLabels: {
+ 'bounds.x': [312.35, 305.98],
+ },
+ },
+ },
+ '../testing/fixtures-lines.stories.tsx.multiSeriesAreaStackedSpec @1280': {
+ reason:
+ 'B1 endpoint full-set width, shared precision and corrected 18px reservation of the already-painted gap; compared against released v8 and pre-RFC93112314 source.',
+ changes: {
+ area: {
+ 'rect.width': [1152.35, 1145.98],
+ },
+ axes: {
+ 'x.end.x': [1175.35, 1168.98],
+ 'x.tick.1.position': [208.96, 208.1],
+ 'x.tick.2.position': [360.58, 358.86],
+ 'x.tick.3.position': [513.87, 511.28],
+ 'x.tick.4.position': [667.16, 663.69],
+ 'x.tick.5.position': [818.78, 814.45],
+ 'x.tick.6.position': [970.41, 965.21],
+ 'x.tick.7.position': [1123.69, 1117.62],
+ },
+ marks: {
+ '0.width': [1116.35, 1109.98],
+ '1.width': [1116.35, 1109.98],
+ '2.width': [1116.35, 1109.98],
+ '3.width': [1116.35, 1109.98],
+ '4.width': [1116.35, 1109.98],
+ '5.width': [1116.35, 1109.98],
+ },
+ endpointLabels: {
+ 'bounds.x': [1193.35, 1186.98],
+ },
+ },
+ },
+ '../testing/fixtures-lines.stories.tsx.lineSpec @1280': {
+ reason:
+ 'B1 endpoint full-set width, shared precision and corrected 18px reservation of the already-painted gap; compared against released v8 and pre-RFC93112314 source.',
+ changes: {
+ area: {
+ 'rect.width': [1015.41, 1013.41],
+ },
+ axes: {
+ 'x.end.x': [1059.41, 1057.41],
+ 'x.tick.1.position': [211.93, 211.65],
+ 'x.tick.2.position': [352.4, 351.84],
+ 'x.tick.3.position': [494.42, 493.57],
+ 'x.tick.4.position': [636.44, 635.3],
+ 'x.tick.5.position': [776.91, 775.49],
+ 'x.tick.6.position': [917.39, 915.68],
+ 'x.tick.7.position': [1059.41, 1057.41],
+ },
+ marks: {
+ '0.width': [986.41, 984.41],
+ '1.width': [986.41, 984.41],
+ '2.width': [986.41, 984.41],
+ },
+ annotations: {
+ 'a0.connector.from.x': [742.02, 740.6],
+ 'a0.connector.to.x': [769.48, 768.05],
+ 'a0.dot.x': [776.91, 775.49],
+ 'a0.label.x': [748.91, 747.49],
+ 'a1.label.x': [297.68, 297.11],
+ },
+ legend: {
+ 'legends.0.bounds.x': [1176.09, 1174.09],
+ 'legends.0.entry.0.x': [1176.09, 1174.09],
+ 'legends.0.entry.1.x': [1176.09, 1174.09],
+ 'legends.0.entry.2.x': [1176.09, 1174.09],
+ },
+ endpointLabels: {
+ 'bounds.x': [1077.41, 1075.41],
+ },
+ },
+ },
+ '../testing/fixtures-misc.stories.tsx.chromeSpec @1280': {
+ reason:
+ 'B1 endpoint full-set width, shared precision and corrected 18px reservation of the already-painted gap; compared against released v8 and pre-RFC93112314 source.',
+ changes: {
+ area: {
+ 'rect.width': [1169.04, 1167.04],
+ },
+ axes: {
+ 'x.end.x': [1192.04, 1190.04],
+ 'x.tick.1.position': [186.92, 186.67],
+ 'x.tick.2.position': [329.83, 329.33],
+ 'x.tick.3.position': [474.32, 473.57],
+ 'x.tick.4.position': [618.81, 617.8],
+ 'x.tick.5.position': [760.15, 758.9],
+ 'x.tick.6.position': [903.07, 901.57],
+ 'x.tick.7.position': [1047.55, 1045.81],
+ 'x.tick.8.position': [1192.04, 1190.04],
+ },
+ marks: {
+ '0.width': [1148.04, 1146.04],
+ '1.width': [1148.04, 1146.04],
+ },
+ endpointLabels: {
+ 'bounds.x': [1210.04, 1208.04],
+ },
+ },
+ },
+ '../testing/fixtures-misc.stories.tsx.fullChromeSpec @1280': {
+ reason:
+ 'B1 endpoint full-set width, shared precision and corrected 18px reservation of the already-painted gap; compared against released v8 and pre-RFC93112314 source.',
+ changes: {
+ area: {
+ 'rect.width': [1169.04, 1167.04],
+ },
+ axes: {
+ 'x.end.x': [1192.04, 1190.04],
+ 'x.tick.1.position': [186.92, 186.67],
+ 'x.tick.2.position': [329.83, 329.33],
+ 'x.tick.3.position': [474.32, 473.57],
+ 'x.tick.4.position': [618.81, 617.8],
+ 'x.tick.5.position': [760.15, 758.9],
+ 'x.tick.6.position': [903.07, 901.57],
+ 'x.tick.7.position': [1047.55, 1045.81],
+ 'x.tick.8.position': [1192.04, 1190.04],
+ },
+ marks: {
+ '0.width': [1148.04, 1146.04],
+ '1.width': [1148.04, 1146.04],
+ },
+ endpointLabels: {
+ 'bounds.x': [1210.04, 1208.04],
+ },
+ },
+ },
+ '../testing/fixtures-ranking.stories.tsx.slopeMarketShareSpec @390': {
+ reason:
+ 'B1 endpoint full-set width, shared precision and corrected 18px reservation of the already-painted gap; compared against released v8 and pre-RFC93112314 source.',
+ changes: {
+ area: {
+ 'rect.width': [92.47, 88.47],
+ 'rect.x': [148.77, 150.77],
+ },
+ axes: {
+ 'x.end.x': [241.23, 239.23],
+ 'x.start.x': [148.77, 150.77],
+ 'x.tick.0.position': [148.77, 150.77],
+ 'x.tick.1.position': [241.23, 239.23],
+ },
+ marks: {
+ '0.width': [92.47, 88.47],
+ '0.x': [148.77, 150.77],
+ '1.width': [92.47, 88.47],
+ '1.x': [148.77, 150.77],
+ '2.width': [92.47, 88.47],
+ '2.x': [148.77, 150.77],
+ '3.width': [92.47, 88.47],
+ '3.x': [148.77, 150.77],
+ '4.width': [92.47, 88.47],
+ '4.x': [148.77, 150.77],
+ },
+ endpointLabels: {
+ 'bounds.x': [259.23, 257.23],
+ 'leadingBounds.x': [12, 14],
+ },
+ },
+ },
+ '../testing/fixtures-ranking.stories.tsx.slopeMarketShareSpec @1280': {
+ reason:
+ 'B1 endpoint full-set width, shared precision and corrected 18px reservation of the already-painted gap; compared against released v8 and pre-RFC93112314 source.',
+ changes: {
+ area: {
+ 'rect.width': [964.47, 960.47],
+ 'rect.x': [157.77, 159.77],
+ },
+ axes: {
+ 'x.end.x': [1122.23, 1120.23],
+ 'x.start.x': [157.77, 159.77],
+ 'x.tick.0.position': [157.77, 159.77],
+ 'x.tick.1.position': [1122.23, 1120.23],
+ },
+ marks: {
+ '0.width': [964.47, 960.47],
+ '0.x': [157.77, 159.77],
+ '1.width': [964.47, 960.47],
+ '1.x': [157.77, 159.77],
+ '2.width': [964.47, 960.47],
+ '2.x': [157.77, 159.77],
+ '3.width': [964.47, 960.47],
+ '3.x': [157.77, 159.77],
+ '4.width': [964.47, 960.47],
+ '4.x': [157.77, 159.77],
+ },
+ endpointLabels: {
+ 'bounds.x': [1140.23, 1138.23],
+ 'leadingBounds.x': [21, 23],
+ },
+ },
+ },
+ '../testing/fixtures-ranking.stories.tsx.bumpConstructorsSpec @390': {
+ reason:
+ 'B1 endpoint full-set width, shared precision and corrected 18px reservation of the already-painted gap; compared against released v8 and pre-RFC93112314 source.',
+ changes: {
+ area: {
+ 'rect.width': [150.15, 146.15],
+ 'rect.x': [119.92, 121.92],
+ },
+ axes: {
+ 'x.end.x': [270.08, 268.08],
+ 'x.start.x': [119.92, 121.92],
+ 'x.tick.0.position': [146.92, 148.92],
+ 'x.tick.1.position': [196.18, 196.58],
+ 'x.tick.2.position': [245.45, 244.25],
+ 'y.end.x': [119.92, 121.92],
+ 'y.start.x': [119.92, 121.92],
+ },
+ marks: {
+ '0.width': [123.15, 119.15],
+ '0.x': [146.92, 148.92],
+ '1.cx': [146.92, 148.92],
+ '10.cx': [196.18, 196.58],
+ '11.cx': [220.82, 220.42],
+ '12.cx': [245.45, 244.25],
+ '13.cx': [270.08, 268.08],
+ '14.width': [123.15, 119.15],
+ '14.x': [146.92, 148.92],
+ '15.cx': [146.92, 148.92],
+ '16.cx': [171.55, 172.75],
+ '17.cx': [196.18, 196.58],
+ '18.cx': [220.82, 220.42],
+ '19.cx': [245.45, 244.25],
+ '2.cx': [171.55, 172.75],
+ '20.cx': [270.08, 268.08],
+ '21.width': [123.15, 119.15],
+ '21.x': [146.92, 148.92],
+ '22.cx': [146.92, 148.92],
+ '23.cx': [171.55, 172.75],
+ '24.cx': [196.18, 196.58],
+ '25.cx': [220.82, 220.42],
+ '26.cx': [245.45, 244.25],
+ '27.cx': [270.08, 268.08],
+ '28.width': [123.15, 119.15],
+ '28.x': [146.92, 148.92],
+ '29.cx': [146.92, 148.92],
+ '3.cx': [196.18, 196.58],
+ '30.cx': [171.55, 172.75],
+ '31.cx': [196.18, 196.58],
+ '32.cx': [220.82, 220.42],
+ '33.cx': [245.45, 244.25],
+ '34.cx': [270.08, 268.08],
+ '4.cx': [220.82, 220.42],
+ '5.cx': [245.45, 244.25],
+ '6.cx': [270.08, 268.08],
+ '7.width': [123.15, 119.15],
+ '7.x': [146.92, 148.92],
+ '8.cx': [146.92, 148.92],
+ '9.cx': [171.55, 172.75],
+ },
+ endpointLabels: {
+ 'bounds.x': [288.08, 286.08],
+ 'leadingBounds.x': [12, 14],
+ },
+ },
+ },
+ '../testing/fixtures-ranking.stories.tsx.bumpConstructorsSpec @1280': {
+ reason:
+ 'B1 endpoint full-set width, shared precision and corrected 18px reservation of the already-painted gap; compared against released v8 and pre-RFC93112314 source.',
+ changes: {
+ area: {
+ 'rect.width': [1022.15, 1018.15],
+ 'rect.x': [128.92, 130.92],
+ },
+ axes: {
+ 'x.end.x': [1151.08, 1149.08],
+ 'x.start.x': [128.92, 130.92],
+ 'x.tick.0.position': [155.92, 157.92],
+ 'x.tick.1.position': [354.95, 356.15],
+ 'x.tick.2.position': [553.98, 554.38],
+ 'x.tick.3.position': [753.02, 752.62],
+ 'x.tick.4.position': [952.05, 950.85],
+ 'x.tick.5.position': [1151.08, 1149.08],
+ 'y.end.x': [128.92, 130.92],
+ 'y.start.x': [128.92, 130.92],
+ },
+ marks: {
+ '0.width': [995.15, 991.15],
+ '0.x': [155.92, 157.92],
+ '1.cx': [155.92, 157.92],
+ '10.cx': [553.98, 554.38],
+ '11.cx': [753.02, 752.62],
+ '12.cx': [952.05, 950.85],
+ '13.cx': [1151.08, 1149.08],
+ '14.width': [995.15, 991.15],
+ '14.x': [155.92, 157.92],
+ '15.cx': [155.92, 157.92],
+ '16.cx': [354.95, 356.15],
+ '17.cx': [553.98, 554.38],
+ '18.cx': [753.02, 752.62],
+ '19.cx': [952.05, 950.85],
+ '2.cx': [354.95, 356.15],
+ '20.cx': [1151.08, 1149.08],
+ '21.width': [995.15, 991.15],
+ '21.x': [155.92, 157.92],
+ '22.cx': [155.92, 157.92],
+ '23.cx': [354.95, 356.15],
+ '24.cx': [553.98, 554.38],
+ '25.cx': [753.02, 752.62],
+ '26.cx': [952.05, 950.85],
+ '27.cx': [1151.08, 1149.08],
+ '28.width': [995.15, 991.15],
+ '28.x': [155.92, 157.92],
+ '29.cx': [155.92, 157.92],
+ '3.cx': [553.98, 554.38],
+ '30.cx': [354.95, 356.15],
+ '31.cx': [553.98, 554.38],
+ '32.cx': [753.02, 752.62],
+ '33.cx': [952.05, 950.85],
+ '34.cx': [1151.08, 1149.08],
+ '4.cx': [753.02, 752.62],
+ '5.cx': [952.05, 950.85],
+ '6.cx': [1151.08, 1149.08],
+ '7.width': [995.15, 991.15],
+ '7.x': [155.92, 157.92],
+ '8.cx': [155.92, 157.92],
+ '9.cx': [354.95, 356.15],
+ },
+ endpointLabels: {
+ 'bounds.x': [1169.08, 1167.08],
+ 'leadingBounds.x': [21, 23],
+ },
+ },
+ },
+ '../testing/fixtures-refresh.stories.tsx.presetSpec @1280': {
+ reason:
+ 'B1 endpoint full-set width, shared precision and corrected 18px reservation of the already-painted gap; compared against released v8 and pre-RFC93112314 source.',
+ changes: {
+ area: {
+ 'rect.width': [1085.06, 1083.06],
+ },
+ axes: {
+ 'x.end.x': [1129.06, 1127.06],
+ 'x.tick.1.position': [273.01, 272.61],
+ 'x.tick.2.position': [487.02, 486.22],
+ 'x.tick.3.position': [701.04, 699.84],
+ 'x.tick.4.position': [915.05, 913.45],
+ 'x.tick.5.position': [1129.06, 1127.06],
+ },
+ marks: {
+ '0.width': [1070.06, 1068.06],
+ '1.width': [1070.06, 1068.06],
+ },
+ endpointLabels: {
+ 'bounds.x': [1147.06, 1145.06],
+ },
+ },
+ },
+};
+
+/** B1: point category keys paint in their resolved top reservation. Released
+ * v8 reserved the top band but painted the key at the bottom. */
+export const APPROVED_MAP_KEY_PLACEMENT: Record<
+ string,
+ (typeof APPROVED_RFC_B1_LAYOUT)[string] & { style: Record }
+> = {
+ '../gallery/maps.stories.tsx.pointLayerSpec @390': {
+ reason:
+ 'B1: paint the point category key in its existing resolved top reservation; v8 incorrectly painted it at the bottom without reserving a bottom band.',
+ style: { 'legends.pointCategoricalLegend.position': ['bottom', 'top'] },
+ changes: {
+ legend: {
+ 'pointCategoricalLegend.bounds.y': [327.4, 74.85],
+ 'pointCategoricalLegend.entry.0.x': [undefined, 10],
+ 'pointCategoricalLegend.entry.0.y': [undefined, 74.85],
+ 'pointCategoricalLegend.entry.1.x': [undefined, 49.34],
+ 'pointCategoricalLegend.entry.1.y': [undefined, 74.85],
+ 'pointCategoricalLegend.entry.2.x': [undefined, 88.67],
+ 'pointCategoricalLegend.entry.2.y': [undefined, 74.85],
+ },
+ },
+ },
+ '../gallery/maps.stories.tsx.pointLayerSpec @1280': {
+ reason:
+ 'B1: paint the point category key in its existing resolved top reservation; v8 incorrectly painted it at the bottom without reserving a bottom band.',
+ style: { 'legends.pointCategoricalLegend.position': ['bottom', 'top'] },
+ changes: {
+ legend: {
+ 'pointCategoricalLegend.bounds.y': [307.4, 97.2],
+ 'pointCategoricalLegend.entry.0.x': [undefined, 20],
+ 'pointCategoricalLegend.entry.0.y': [undefined, 97.2],
+ 'pointCategoricalLegend.entry.1.x': [undefined, 59.34],
+ 'pointCategoricalLegend.entry.1.y': [undefined, 97.2],
+ 'pointCategoricalLegend.entry.2.x': [undefined, 98.67],
+ 'pointCategoricalLegend.entry.2.y': [undefined, 97.2],
+ },
+ },
+ },
+ '../gallery/maps.stories.tsx.readerZoomPointsSpec @390': {
+ reason:
+ 'B1: paint the point category key in its existing resolved top reservation; v8 incorrectly painted it at the bottom without reserving a bottom band.',
+ style: { 'legends.pointCategoricalLegend.position': ['bottom', 'top'] },
+ changes: {
+ legend: {
+ 'pointCategoricalLegend.bounds.y': [327.4, 101.3],
+ 'pointCategoricalLegend.entry.0.x': [undefined, 10],
+ 'pointCategoricalLegend.entry.0.y': [undefined, 101.3],
+ 'pointCategoricalLegend.entry.1.x': [undefined, 49.34],
+ 'pointCategoricalLegend.entry.1.y': [undefined, 101.3],
+ 'pointCategoricalLegend.entry.2.x': [undefined, 88.67],
+ 'pointCategoricalLegend.entry.2.y': [undefined, 101.3],
+ },
+ },
+ },
+ '../gallery/maps.stories.tsx.readerZoomPointsSpec @1280': {
+ reason:
+ 'B1: paint the point category key in its existing resolved top reservation; v8 incorrectly painted it at the bottom without reserving a bottom band.',
+ style: { 'legends.pointCategoricalLegend.position': ['bottom', 'top'] },
+ changes: {
+ legend: {
+ 'pointCategoricalLegend.bounds.y': [307.4, 97.2],
+ 'pointCategoricalLegend.entry.0.x': [undefined, 20],
+ 'pointCategoricalLegend.entry.0.y': [undefined, 97.2],
+ 'pointCategoricalLegend.entry.1.x': [undefined, 59.34],
+ 'pointCategoricalLegend.entry.1.y': [undefined, 97.2],
+ 'pointCategoricalLegend.entry.2.x': [undefined, 98.67],
+ 'pointCategoricalLegend.entry.2.y': [undefined, 97.2],
+ },
+ },
+ },
+};
+
+/** B1: alphabetic title baselines must leave room for rotated glyph ascent. */
+export const APPROVED_AXIS_TITLE_LAYOUT: typeof APPROVED_PRE_RFC_LAYOUT = {
+ '../charts/mobile-regression.stories.tsx.inlineYTitleSpec @390': {
+ reason:
+ 'B1 enlarged rotated title no longer clips beyond the viewport; marks, ticks and plot stay unchanged.',
+ changes: { axes: { 'y.titlePosition.x': [19, 21] } },
+ },
+ '../gallery/charts-scatter-distribution.stories.tsx.stripSpec @390': {
+ reason:
+ 'B1 rotated title no longer clips beyond the viewport; marks, ticks and plot stay unchanged.',
+ changes: { axes: { 'y.titlePosition.x': [5.87, 13] } },
+ },
+};
+
+/** B1 collision correction: clamp top-overflow anchors before the separation sweep.
+ * Values and marks are unchanged; only these label centers move into available space. */
+export const APPROVED_ENDPOINT_TOP_SWEEP: typeof APPROVED_PRE_RFC_LAYOUT = {
+ '../gallery/dashboards.layouts.tsx.marketsCompareSpec @1280': {
+ reason:
+ 'Clamp top-overflow endpoint anchors before separation so labels retain their minimum spacing.',
+ changes: {
+ endpointLabels: {
+ '0.labelY': [139.52, 145.45],
+ '2.labelY': [180.77, 186.7],
+ '3.labelY': [222.02, 227.95],
+ },
+ },
+ },
+ '../gallery/dashboards.stories.tsx.crosshairSpec @1280': {
+ reason:
+ 'Clamp top-overflow endpoint anchors before separation so labels retain their minimum spacing.',
+ changes: {
+ endpointLabels: {
+ '0.labelY': [139.52, 145.45],
+ '2.labelY': [180.77, 186.7],
+ '3.labelY': [222.02, 227.95],
+ },
+ },
+ },
+ '../testing/fixtures-lines.stories.tsx.multiSeriesAreaStackedSpec @390': {
+ reason:
+ 'Clamp top-overflow endpoint anchors before separation so labels retain their minimum spacing.',
+ changes: { endpointLabels: { '0.labelY': [226.2, 227.2], '1.labelY': [182.56, 185.95] } },
+ },
+ '../testing/fixtures-lines.stories.tsx.multiSeriesAreaStackedSpec @1280': {
+ reason:
+ 'Clamp top-overflow endpoint anchors before separation so labels retain their minimum spacing.',
+ changes: { endpointLabels: { '1.labelY': [142.77, 145.45] } },
+ },
+};
+
+/** Approved B3 map layout changes; every released path remains guarded exactly. */
+export const APPROVED_B3_MAP_LAYOUT: typeof APPROVED_MAP_KEY_PLACEMENT = {
+ '../gallery/maps.stories.tsx.worldEqualEarthSpec @390': {
+ reason:
+ 'Approved B3 static map captions and neutral-class context reserve measured key space; occupied-bounds size-key placement has no legacy layout opt-out.',
+ changes: {
+ dims: {
+ 'mapSize.height': [166.7, 128.7],
+ },
+ area: {
+ 'rect.height': [166.7, 128.7],
+ 'rect.y': [157.3, 195.3],
+ },
+ marks: {
+ '0.height': [2.85, 2.2],
+ '0.width': [337.62, 260.66],
+ '0.x': [16.19, 54.67],
+ '0.y': [103.46, 79.87],
+ '1.height': [13.81, 10.66],
+ '1.width': [10.23, 7.9],
+ '1.x': [213, 206.62],
+ '1.y': [84.19, 65],
+ '2.height': [8.16, 6.3],
+ '2.width': [7.96, 6.15],
+ '2.x': [169.21, 172.81],
+ '2.y': [48.1, 37.13],
+ '3.height': [31.79, 24.54],
+ '3.width': [66.09, 51.03],
+ '3.x': [83.08, 106.31],
+ '3.y': [0.1, 0.08],
+ '4.height': [53.48, 41.29],
+ '4.width': [92.66, 71.54],
+ '4.x': [37.65, 71.24],
+ '4.y': [5.37, 4.14],
+ '5.height': [15.08, 11.64],
+ '5.width': [32.26, 24.9],
+ '5.x': [222.05, 213.6],
+ '5.y': [17.92, 13.84],
+ '6.height': [9.28, 7.16],
+ '6.width': [15.73, 12.14],
+ '6.x': [230.81, 220.37],
+ '6.y': [27.68, 21.37],
+ '7.height': [10.46, 8.07],
+ '7.width': [14.67, 11.33],
+ '7.x': [318.97, 288.43],
+ '7.y': [86.19, 66.54],
+ '8.height': [20.35, 15.71],
+ '8.width': [43.81, 33.83],
+ '8.x': [275.88, 255.16],
+ '8.y': [75.92, 58.61],
+ '9.height': [37.17, 28.7],
+ '9.width': [14.17, 10.94],
+ '9.x': [122.23, 136.54],
+ '9.y': [110.72, 85.48],
+ '10.height': [42.81, 33.05],
+ '10.width': [15.01, 11.59],
+ '10.x': [119.38, 134.34],
+ '10.y': [105.42, 81.39],
+ '11.height': [23.75, 18.34],
+ '11.width': [18.17, 14.03],
+ '11.x': [196.61, 193.97],
+ '11.y': [76.21, 58.84],
+ '12.height': [17.59, 13.58],
+ '12.width': [9.24, 7.13],
+ '12.x': [224.14, 215.22],
+ '12.y': [67.55, 52.15],
+ '13.height': [13.1, 10.11],
+ '13.width': [7.57, 5.84],
+ '13.x': [217.39, 210.01],
+ '13.y': [75.89, 58.59],
+ '14.height': [17.38, 13.42],
+ '14.width': [15.12, 11.68],
+ '14.x': [205.72, 201],
+ '14.y': [55.01, 42.47],
+ '15.height': [20.16, 15.56],
+ '15.width': [9.68, 7.47],
+ '15.x': [197.75, 194.84],
+ '15.y': [53.27, 41.13],
+ '16.height': [2.36, 1.82],
+ '16.width': [2.82, 2.18],
+ '16.x': [115.57, 131.4],
+ '16.y': [57.6, 44.47],
+ '17.height': [2.86, 2.21],
+ '17.width': [3.38, 2.61],
+ '17.x': [117.97, 133.25],
+ '17.y': [57.64, 44.5],
+ '18.height': [31.79, 24.55],
+ '18.width': [249.21, 192.4],
+ '18.x': [61.86, 89.93],
+ '18.y': [0.67, 0.52],
+ '19.height': [4.06, 3.14],
+ '19.width': [1.83, 1.41],
+ '19.x': [113.33, 129.67],
+ '19.y': [48.84, 37.71],
+ '20.height': [1.18, 0.91],
+ '20.width': [2.56, 1.98],
+ '20.x': [137.49, 148.32],
+ '20.y': [143.91, 111.1],
+ '21.height': [14.61, 11.28],
+ '21.width': [16.74, 12.93],
+ '21.x': [188.52, 187.72],
+ '21.y': [0.86, 0.67],
+ '22.height': [13.77, 10.63],
+ '22.width': [36.79, 28.4],
+ '22.x': [141.08, 151.09],
+ '23.height': [1.16, 0.9],
+ '23.width': [1.73, 1.33],
+ '23.x': [239.29, 226.92],
+ '23.y': [141.42, 109.18],
+ '24.height': [1.43, 1.11],
+ '24.width': [2.32, 1.79],
+ '24.x': [303.75, 276.68],
+ '24.y': [93.6, 72.27],
+ '25.height': [15.28, 11.8],
+ '25.width': [15.06, 11.62],
+ '25.x': [199.7, 196.35],
+ '25.y': [111.04, 85.73],
+ '26.height': [2.39, 1.84],
+ '26.width': [2.15, 1.66],
+ '26.x': [209.15, 203.64],
+ '26.y': [119.04, 91.9],
+ '27.height': [22.29, 17.21],
+ '27.width': [23.64, 18.25],
+ '27.x': [81.12, 104.8],
+ '27.y': [42.07, 32.48],
+ '28.height': [5.69, 4.39],
+ '28.width': [4.65, 3.59],
+ '28.x': [133.5, 145.24],
+ '28.y': [120.79, 93.25],
+ '29.height': [48.88, 37.73],
+ '29.width': [37.36, 28.84],
+ '29.x': [114.58, 130.63],
+ '29.y': [76.22, 58.85],
+ '30.height': [16.5, 12.74],
+ '30.width': [12.23, 9.45],
+ '30.x': [119.13, 134.14],
+ '30.y': [95.51, 73.74],
+ '31.height': [23.34, 18.02],
+ '31.width': [13.07, 10.09],
+ '31.x': [107.32, 125.03],
+ '31.y': [83.04, 64.11],
+ '32.height': [21.48, 16.58],
+ '32.width': [11.57, 8.93],
+ '32.x': [109.52, 126.73],
+ '32.y': [67.02, 51.74],
+ '33.height': [3.06, 2.36],
+ '33.width': [5.42, 4.18],
+ '33.x': [106.1, 124.09],
+ '33.y': [70.63, 54.53],
+ '34.height': [3.82, 2.95],
+ '34.width': [3.06, 2.36],
+ '34.x': [103.57, 122.13],
+ '34.y': [68.58, 52.94],
+ '35.height': [5.45, 4.21],
+ '35.width': [4.6, 3.55],
+ '35.x': [102.23, 121.09],
+ '35.y': [63.75, 49.22],
+ '36.height': [3.82, 2.95],
+ '36.width': [5.94, 4.59],
+ '36.x': [100.89, 120.06],
+ '36.y': [62.5, 48.26],
+ '37.height': [1.62, 1.25],
+ '37.width': [2.24, 1.73],
+ '37.x': [100.07, 119.43],
+ '37.y': [64.5, 49.8],
+ '38.height': [5.15, 3.98],
+ '38.width': [3.99, 3.08],
+ '38.x': [98.21, 117.99],
+ '38.y': [60.22, 46.49],
+ '39.height': [3.28, 2.54],
+ '39.width': [1.57, 1.21],
+ '39.x': [101.28, 120.37],
+ '39.y': [59.37, 45.84],
+ '40.height': [14.66, 11.32],
+ '40.width': [12.81, 9.89],
+ '40.x': [115.37, 131.24],
+ '40.y': [67.37, 52.02],
+ '41.height': [9.12, 7.04],
+ '41.width': [4.52, 3.49],
+ '41.x': [126.46, 139.8],
+ '41.y': [72.22, 55.76],
+ '42.height': [5.41, 4.18],
+ '42.width': [3.96, 3.06],
+ '42.x': [129.59, 142.22],
+ '42.y': [75.22, 58.07],
+ '43.height': [58.34, 45.04],
+ '43.width': [60.08, 46.38],
+ '43.x': [132.91, 144.78],
+ '43.y': [21.98, 16.97],
+ '44.height': [8.16, 6.3],
+ '44.width': [5.45, 4.21],
+ '44.x': [107.65, 125.28],
+ '44.y': [81.19, 62.68],
+ '45.height': [0.72, 0.56],
+ '45.width': [1.53, 1.18],
+ '45.x': [122.29, 136.58],
+ '45.y': [59.34, 45.82],
+ '46.height': [1.03, 0.8],
+ '46.width': [1.93, 1.49],
+ '46.x': [111.93, 128.59],
+ '46.y': [59.34, 45.81],
+ '47.height': [4.13, 3.19],
+ '47.width': [9.61, 7.42],
+ '47.x': [106.6, 124.47],
+ '47.y': [53.54, 41.34],
+ '48.height': [8.46, 6.53],
+ '48.width': [7.16, 5.53],
+ '48.x': [208.59, 203.22],
+ '48.y': [102.81, 79.37],
+ '49.height': [11.32, 8.74],
+ '49.width': [8.96, 6.92],
+ '49.x': [203.17, 199.03],
+ '49.y': [105.52, 81.47],
+ '50.height': [14.9, 11.5],
+ '50.width': [12.46, 9.62],
+ '50.x': [195.97, 193.47],
+ '50.y': [104.62, 80.77],
+ '51.height': [5.4, 4.17],
+ '51.width': [5.75, 4.44],
+ '51.x': [168.42, 172.2],
+ '51.y': [61.76, 47.68],
+ '52.height': [18.65, 14.4],
+ '52.width': [15.45, 11.93],
+ '52.x': [173.55, 176.16],
+ '52.y': [51.35, 39.65],
+ '53.height': [15.85, 12.23],
+ '53.width': [11.29, 8.72],
+ '53.x': [169.21, 172.81],
+ '53.y': [48.41, 37.37],
+ '54.height': [7.79, 6.01],
+ '54.width': [2.87, 2.21],
+ '54.x': [185.73, 185.57],
+ '54.y': [67.28, 51.94],
+ '55.height': [14.81, 11.44],
+ '55.width': [14.46, 11.17],
+ '55.x': [185.28, 185.22],
+ '55.y': [53.2, 41.07],
+ '56.height': [12.3, 9.5],
+ '56.width': [11.22, 8.66],
+ '56.x': [187.56, 186.98],
+ '56.y': [65.21, 50.34],
+ '57.height': [14.26, 11.01],
+ '57.width': [7.2, 5.56],
+ '57.x': [193.1, 191.25],
+ '57.y': [66.49, 51.33],
+ '58.height': [6.51, 5.03],
+ '58.width': [1.83, 1.41],
+ '58.x': [184.95, 184.96],
+ '58.y': [68.83, 53.14],
+ '59.height': [8.18, 6.32],
+ '59.width': [4.1, 3.17],
+ '59.x': [181.91, 182.61],
+ '59.y': [68.73, 53.06],
+ '60.height': [7.93, 6.12],
+ '60.width': [5.76, 4.45],
+ '60.x': [176.8, 178.67],
+ '60.y': [69.46, 53.63],
+ '61.height': [6.74, 5.2],
+ '61.width': [6.89, 5.32],
+ '61.x': [170.67, 173.94],
+ '61.y': [66.83, 51.6],
+ '62.height': [2.02, 1.56],
+ '62.width': [2.82, 2.18],
+ '62.x': [169.24, 172.83],
+ '62.y': [66.78, 51.56],
+ '63.height': [5.37, 4.15],
+ '63.width': [3.7, 2.86],
+ '63.x': [174.1, 176.59],
+ '63.y': [71.99, 55.58],
+ '64.height': [4.18, 3.22],
+ '64.width': [2.86, 2.21],
+ '64.x': [172.41, 175.28],
+ '64.y': [70.07, 54.1],
+ '65.height': [7, 5.41],
+ '65.width': [7.24, 5.59],
+ '65.x': [179.82, 181],
+ '65.y': [63.63, 49.12],
+ '66.height': [11.38, 8.79],
+ '66.width': [12.32, 9.51],
+ '66.x': [198.79, 195.65],
+ '66.y': [68.67, 53.02],
+ '67.height': [11.28, 8.71],
+ '67.width': [7.03, 5.43],
+ '67.x': [195.59, 193.18],
+ '67.y': [78.17, 60.35],
+ '68.height': [8.12, 6.27],
+ '68.width': [5.38, 4.15],
+ '68.x': [193.41, 191.49],
+ '68.y': [79.98, 61.75],
+ '69.height': [1.64, 1.27],
+ '69.width': [1.89, 1.46],
+ '69.x': [193.89, 191.86],
+ '69.y': [80.03, 61.79],
+ '70.height': [12.34, 9.53],
+ '70.width': [11.22, 8.66],
+ '70.x': [205.53, 200.85],
+ '70.y': [93.56, 72.23],
+ '71.height': [9.61, 7.42],
+ '71.width': [2.76, 2.13],
+ '71.x': [215.81, 208.79],
+ '71.y': [94.83, 73.21],
+ '72.height': [20.52, 15.84],
+ '72.width': [9.97, 7.7],
+ '72.x': [213.39, 206.92],
+ '72.y': [96.22, 74.28],
+ '73.height': [1.97, 1.52],
+ '73.width': [1.26, 0.97],
+ '73.x': [212.82, 206.47],
+ '73.y': [115.42, 89.11],
+ '74.height': [17.18, 13.26],
+ '74.width': [11.86, 9.16],
+ '74.x': [195.9, 193.42],
+ '74.y': [88.68, 68.47],
+ '75.height': [2.77, 2.14],
+ '75.width': [1.64, 1.27],
+ '75.x': [212.72, 206.4],
+ '75.y': [85.99, 66.39],
+ '76.height': [4.47, 3.45],
+ '76.width': [1.14, 0.88],
+ '76.x': [215.46, 208.52],
+ '76.y': [41.41, 31.97],
+ '77.height': [1.81, 1.4],
+ '77.width': [1.12, 0.87],
+ '77.x': [215.94, 208.89],
+ '77.y': [39.82, 30.74],
+ '78.height': [16.94, 13.08],
+ '78.width': [7.56, 5.83],
+ '78.x': [224.87, 215.79],
+ '78.y': [98.41, 75.98],
+ '79.height': [1.39, 1.07],
+ '79.width': [0.52, 0.4],
+ '79.x': [215.99, 208.93],
+ '79.y': [42.29, 32.65],
+ '80.height': [0.95, 0.73],
+ '80.width': [2.84, 2.19],
+ '80.x': [169.11, 172.73],
+ '80.y': [65.2, 50.33],
+ '81.height': [8.2, 6.33],
+ '81.width': [3.52, 2.72],
+ '81.x': [191.59, 190.09],
+ '81.y': [36.72, 28.35],
+ '82.height': [21.69, 16.75],
+ '82.width': [18.87, 14.56],
+ '82.x': [177.15, 178.94],
+ '82.y': [36.98, 28.55],
+ '83.height': [4.95, 3.82],
+ '83.width': [3.39, 2.62],
+ '83.x': [216.29, 209.16],
+ '83.y': [41.3, 31.88],
+ '84.height': [4.36, 3.37],
+ '84.width': [4.28, 3.3],
+ '84.x': [232.2, 221.44],
+ '84.y': [50.04, 38.63],
+ '85.height': [1.9, 1.47],
+ '85.width': [0.83, 0.64],
+ '85.x': [231.23, 220.69],
+ '85.y': [49.96, 38.57],
+ '86.height': [1.84, 1.42],
+ '86.width': [1.76, 1.36],
+ '86.x': [226.8, 217.27],
+ '86.y': [45.21, 34.91],
+ '87.height': [9.68, 7.48],
+ '87.width': [9.31, 7.19],
+ '87.x': [219.12, 211.34],
+ '87.y': [36.68, 28.32],
+ '88.height': [12.07, 9.32],
+ '88.width': [6.7, 5.17],
+ '88.x': [233.4, 222.37],
+ '88.y': [49.62, 38.31],
+ '89.height': [2.49, 1.92],
+ '89.width': [0.73, 0.56],
+ '89.x': [341.53, 305.85],
+ '89.y': [101.7, 78.51],
+ '90.height': [5.19, 4.01],
+ '90.width': [5.05, 3.9],
+ '90.x': [281.54, 259.53],
+ '90.y': [64.32, 49.66],
+ '91.height': [18.67, 14.42],
+ '91.width': [8.51, 6.57],
+ '91.x': [275.77, 255.08],
+ '91.y': [56.97, 43.99],
+ '92.height': [10.75, 8.3],
+ '92.width': [8.33, 6.43],
+ '92.x': [277.8, 256.64],
+ '92.y': [54.44, 42.03],
+ '93.height': [22.94, 17.71],
+ '93.width': [8.92, 6.89],
+ '93.x': [270.27, 250.84],
+ '93.y': [47.28, 36.5],
+ '94.height': [18.58, 14.34],
+ '94.width': [9.25, 7.14],
+ '94.x': [279.09, 257.64],
+ '94.y': [53.34, 41.18],
+ '95.height': [5.9, 4.55],
+ '95.width': [4.38, 3.38],
+ '95.x': [290.31, 266.3],
+ '95.y': [30.46, 23.52],
+ '96.height': [4.83, 3.73],
+ '96.width': [4.19, 3.23],
+ '96.x': [293.34, 268.64],
+ '96.y': [35.29, 27.25],
+ '97.height': [10.87, 8.39],
+ '97.width': [27.23, 21.03],
+ '97.x': [254.57, 238.71],
+ '97.y': [21.1, 16.29],
+ '98.height': [33.89, 26.16],
+ '98.width': [25.41, 19.62],
+ '98.x': [247.52, 233.27],
+ '98.y': [38.84, 29.99],
+ '99.height': [7.1, 5.48],
+ '99.width': [5.37, 4.15],
+ '99.x': [265.27, 246.97],
+ '99.y': [49.56, 38.26],
+ '100.height': [1.91, 1.47],
+ '100.width': [3.07, 2.37],
+ '100.x': [265.33, 247.02],
+ '100.y': [47.32, 36.54],
+ '101.height': [4.84, 3.74],
+ '101.width': [7.95, 6.14],
+ '101.x': [256.98, 240.57],
+ '101.y': [44.78, 34.57],
+ '102.height': [15.96, 12.32],
+ '102.width': [13.24, 10.22],
+ '102.x': [239.46, 227.05],
+ '102.y': [36.96, 28.54],
+ '103.height': [10.67, 8.23],
+ '103.width': [11.62, 8.97],
+ '103.x': [238.15, 226.04],
+ '103.y': [35.43, 27.36],
+ '104.height': [4.74, 3.66],
+ '104.width': [7.08, 5.46],
+ '104.x': [242.43, 229.34],
+ '104.y': [32.67, 25.22],
+ '105.height': [4.42, 3.41],
+ '105.width': [8.08, 6.24],
+ '105.x': [243.88, 230.46],
+ '105.y': [30.12, 23.25],
+ '106.height': [8.39, 6.48],
+ '106.width': [13.28, 10.26],
+ '106.x': [228.97, 218.94],
+ '106.y': [30.71, 23.71],
+ '107.height': [17.17, 13.26],
+ '107.width': [19.9, 15.36],
+ '107.x': [222.5, 213.95],
+ '107.y': [34.06, 26.29],
+ '108.height': [5.69, 4.39],
+ '108.width': [5.23, 4.04],
+ '108.x': [216.24, 209.12],
+ '108.y': [36.85, 28.45],
+ '109.height': [2.79, 2.15],
+ '109.width': [3.03, 2.34],
+ '109.x': [221.66, 213.31],
+ '109.y': [32.36, 24.98],
+ '110.height': [11.13, 8.59],
+ '110.width': [8.14, 6.29],
+ '110.x': [193.03, 191.2],
+ '110.y': [6.81, 5.26],
+ '111.height': [4.62, 3.57],
+ '111.width': [7.15, 5.52],
+ '111.x': [202.91, 198.83],
+ '111.y': [17.2, 13.28],
+ '112.height': [7.16, 5.53],
+ '112.width': [14.27, 11.02],
+ '112.x': [202.63, 198.61],
+ '112.y': [20.82, 16.07],
+ '113.height': [5.69, 4.39],
+ '113.width': [8.05, 6.22],
+ '113.x': [195.78, 193.32],
+ '113.y': [18.42, 14.22],
+ '114.height': [2.69, 2.07],
+ '114.width': [5.94, 4.59],
+ '114.x': [192.65, 190.9],
+ '114.y': [24.1, 18.6],
+ '115.height': [2.97, 2.3],
+ '115.width': [5.12, 3.95],
+ '115.x': [198.09, 195.11],
+ '115.y': [24.52, 18.93],
+ '116.height': [3.1, 2.39],
+ '116.width': [3.05, 2.36],
+ '116.x': [206.29, 201.43],
+ '116.y': [24.68, 19.05],
+ '117.height': [4.77, 3.68],
+ '117.width': [7.83, 6.05],
+ '117.x': [201.43, 197.68],
+ '117.y': [24.93, 19.25],
+ '118.height': [2.3, 1.78],
+ '118.width': [4.3, 3.32],
+ '118.x': [200.75, 197.16],
+ '118.y': [17.01, 13.13],
+ '119.height': [2.14, 1.65],
+ '119.width': [5.38, 4.16],
+ '119.x': [200.67, 197.09],
+ '119.y': [15.57, 12.02],
+ '120.height': [1.88, 1.45],
+ '120.width': [3.46, 2.67],
+ '120.x': [201.94, 198.08],
+ '120.y': [14.13, 10.91],
+ '121.height': [7.59, 5.86],
+ '121.width': [7.08, 5.47],
+ '121.x': [189.65, 188.59],
+ '121.y': [18.29, 14.12],
+ '122.height': [3.26, 2.52],
+ '122.width': [5.12, 3.95],
+ '122.x': [203.49, 199.27],
+ '122.y': [29.11, 22.48],
+ '123.height': [7.79, 6.01],
+ '123.width': [5.78, 4.46],
+ '123.x': [202.11, 198.21],
+ '123.y': [31.72, 24.49],
+ '124.height': [7.09, 5.47],
+ '124.width': [16.71, 12.9],
+ '124.x': [206.86, 201.88],
+ '124.y': [31.38, 24.22],
+ '125.height': [3.38, 2.61],
+ '125.width': [1.6, 1.24],
+ '125.x': [201.12, 197.45],
+ '125.y': [30.78, 23.76],
+ '126.height': [4.3, 3.32],
+ '126.width': [4.69, 3.62],
+ '126.x': [196.16, 193.62],
+ '126.y': [26.71, 20.62],
+ '127.height': [2.14, 1.65],
+ '127.width': [3.55, 2.74],
+ '127.x': [189.88, 188.77],
+ '127.y': [25.33, 19.56],
+ '128.height': [0.69, 0.53],
+ '128.width': [0.44, 0.34],
+ '128.x': [189.49, 188.47],
+ '128.y': [23, 17.76],
+ '129.height': [1.94, 1.5],
+ '129.width': [2.86, 2.21],
+ '129.x': [186.96, 186.52],
+ '129.y': [21.66, 16.72],
+ '130.height': [2.64, 2.04],
+ '130.width': [2.86, 2.21],
+ '130.x': [187.58, 186.99],
+ '130.y': [19.69, 15.2],
+ '131.height': [6.08, 4.69],
+ '131.width': [2.77, 2.14],
+ '131.x': [176.87, 178.72],
+ '131.y': [31.22, 24.11],
+ '132.height': [8.69, 6.71],
+ '132.width': [10.35, 7.99],
+ '132.x': [177.2, 178.98],
+ '132.y': [29.63, 22.88],
+ '133.height': [3.32, 2.56],
+ '133.width': [3.12, 2.41],
+ '133.x': [177.25, 179.02],
+ '133.y': [18.16, 14.02],
+ '134.height': [2.85, 2.2],
+ '134.width': [1.9, 1.47],
+ '134.x': [337.15, 302.47],
+ '134.y': [108.58, 83.83],
+ '135.height': [5.41, 4.18],
+ '135.width': [4.82, 3.72],
+ '135.x': [334.06, 300.08],
+ '135.y': [91.46, 70.61],
+ '136.height': [13.48, 10.41],
+ '136.width': [18.02, 13.91],
+ '136.x': [320.31, 289.47],
+ '136.y': [125.89, 97.2],
+ '137.height': [39.52, 30.51],
+ '137.width': [38.62, 29.82],
+ '137.x': [285.74, 262.77],
+ '137.y': [96.66, 74.63],
+ '138.height': [4.94, 3.81],
+ '138.width': [2.05, 1.58],
+ '138.x': [260.79, 243.52],
+ '138.y': [70.35, 54.32],
+ '139.height': [40.01, 30.89],
+ '139.width': [47.06, 36.33],
+ '139.x': [247.57, 233.31],
+ '139.y': [19.74, 15.24],
+ '140.height': [4.09, 3.16],
+ '140.width': [1.28, 0.98],
+ '140.x': [295.19, 270.07],
+ '140.y': [50.96, 39.34],
+ '141.height': [11.47, 8.86],
+ '141.width': [10.11, 7.81],
+ '141.x': [190.53, 189.27],
+ '141.y': [26.07, 20.13],
+ '142.height': [2.68, 2.07],
+ '142.width': [3.5, 2.71],
+ '142.x': [191.02, 189.65],
+ '142.y': [15.78, 12.19],
+ '143.height': [8.19, 6.32],
+ '143.width': [7.06, 5.45],
+ '143.x': [179.23, 180.55],
+ '143.y': [14.98, 11.57],
+ '144.height': [2.3, 1.78],
+ '144.width': [7.24, 5.59],
+ '144.x': [168.47, 172.24],
+ '144.y': [8.62, 6.65],
+ '145.height': [3.99, 3.08],
+ '145.width': [4.82, 3.72],
+ '145.x': [222.79, 214.18],
+ '145.y': [31.68, 24.46],
+ '146.height': [2.71, 2.1],
+ '146.width': [6.12, 4.72],
+ '146.x': [218.09, 210.55],
+ '146.y': [29.84, 23.04],
+ '147.height': [16.43, 12.68],
+ '147.width': [9.06, 7],
+ '147.x': [296.42, 271.02],
+ '147.y': [59.36, 45.83],
+ '148.height': [7.91, 6.11],
+ '148.width': [18.3, 14.13],
+ '148.x': [280.36, 258.62],
+ '148.y': [74.06, 57.18],
+ '149.height': [1.85, 1.43],
+ '149.width': [1.12, 0.86],
+ '149.x': [293.99, 269.14],
+ '149.y': [75.96, 58.65],
+ '150.height': [1.47, 1.13],
+ '150.width': [2.28, 1.76],
+ '150.x': [196.14, 193.6],
+ '150.y': [26.35, 20.34],
+ '151.height': [7.81, 6.03],
+ '151.width': [8.52, 6.58],
+ '151.x': [198.52, 195.44],
+ '151.y': [6.12, 4.72],
+ '152.height': [1.85, 1.43],
+ '152.width': [4.45, 3.43],
+ '152.x': [198.47, 195.4],
+ '152.y': [23.56, 18.19],
+ '153.height': [2.58, 1.99],
+ '153.width': [5.3, 4.09],
+ '153.x': [194.63, 192.43],
+ '153.y': [22.01, 17],
+ '154.height': [7, 5.4],
+ '154.width': [6.53, 5.04],
+ '154.x': [219.16, 211.37],
+ '154.y': [60, 46.32],
+ '155.height': [16.35, 12.62],
+ '155.width': [8.35, 6.45],
+ '155.x': [298.86, 272.91],
+ '155.y': [27.71, 21.4],
+ '156.height': [10.09, 7.79],
+ '156.width': [8.14, 6.28],
+ '156.x': [127.23, 140.4],
+ '156.y': [107.63, 83.09],
+ '157.height': [8.09, 6.25],
+ '157.width': [9.7, 7.49],
+ '157.x': [225.03, 215.91],
+ '157.y': [58.74, 45.35],
+ '158.height': [19.35, 14.94],
+ '158.width': [20.12, 15.53],
+ '158.x': [216.22, 209.11],
+ '158.y': [42.72, 32.99],
+ '159.height': [11.86, 9.16],
+ '159.width': [212.22, 163.85],
+ '159.x': [81.85, 105.36],
+ '159.y': [154.84, 119.54],
+ '160.height': [0.77, 0.6],
+ '160.width': [1.52, 1.17],
+ '160.x': [213.52, 207.02],
+ '160.y': [38.64, 29.83],
+ '161.height': [0.7, 0.54],
+ '161.width': [1.54, 1.19],
+ '161.x': [213.12, 206.71],
+ '161.y': [39.21, 30.27],
+ '162.height': [17.19, 13.27],
+ '162.width': [14.73, 11.37],
+ '162.x': [169.27, 172.86],
+ '162.y': [38.54, 29.75],
+ '163.height': [11.61, 8.96],
+ '163.width': [11.94, 9.22],
+ '163.x': [207.06, 202.03],
+ '163.y': [43.4, 33.51],
+ '164.height': [16.44, 12.69],
+ '164.width': [14.74, 11.38],
+ '164.x': [193.46, 191.53],
+ '164.y': [41.58, 32.1],
+ '165.height': [14.74, 11.38],
+ '165.width': [14.1, 10.89],
+ '165.x': [216.35, 209.21],
+ '165.y': [63.82, 49.28],
+ '166.height': [2.26, 1.74],
+ '166.width': [1.51, 1.17],
+ '166.x': [224.43, 215.44],
+ '166.y': [66.69, 51.49],
+ '167.height': [4.43, 3.42],
+ '167.width': [6.12, 4.73],
+ '167.x': [225.34, 216.15],
+ '167.y': [68.26, 52.7],
+ '168.height': [7.33, 5.66],
+ '168.width': [5.21, 4.02],
+ '168.x': [213.27, 206.82],
+ '168.y': [77.5, 59.83],
+ '169.height': [2.29, 1.77],
+ '169.width': [1.72, 1.33],
+ '169.x': [212.72, 206.4],
+ '169.y': [84.43, 65.19],
+ '170.height': [2.77, 2.14],
+ '170.width': [3.25, 2.51],
+ '170.x': [197.92, 194.97],
+ '170.y': [28.05, 21.65],
+ '171.height': [1.62, 1.25],
+ '171.width': [2.11, 1.63],
+ '171.x': [202.17, 198.25],
+ '171.y': [31.18, 24.07],
+ '172.height': [4.2, 3.25],
+ '172.width': [3.74, 2.89],
+ '172.x': [200.32, 196.83],
+ '172.y': [27.06, 20.89],
+ '173.height': [1.79, 1.38],
+ '173.width': [1.53, 1.18],
+ '173.x': [200.38, 196.87],
+ '173.y': [29.88, 23.07],
+ '174.height': [1.55, 1.2],
+ '174.width': [1.41, 1.09],
+ '174.x': [201.72, 197.91],
+ '174.y': [30.15, 23.27],
+ '175.height': [1.14, 0.88],
+ '175.width': [1.07, 0.82],
+ '175.x': [126.23, 139.63],
+ '175.y': [68.99, 53.27],
+ '176.height': [11.19, 8.64],
+ '176.width': [10.95, 8.46],
+ '176.x': [207.71, 202.53],
+ '176.y': [67.26, 51.93],
+ },
+ legend: {
+ 'continuousLegend.bounds.height': [61, 99],
+ 'continuousLegend.bar.y': [111.3, 126.3],
+ },
+ },
+ style: {
+ 'legends.continuousLegend.title': ['gdp ($)', 'gdp'],
+ },
+ },
+ '../gallery/maps.stories.tsx.worldEqualEarthSpec @1280': {
+ reason:
+ 'Approved B3 static map captions and neutral-class context reserve measured key space; occupied-bounds size-key placement has no legacy layout opt-out.',
+ changes: {
+ dims: {
+ 'mapSize.height': [162, 124],
+ },
+ area: {
+ 'rect.height': [162, 124],
+ 'rect.y': [153.2, 191.2],
+ },
+ marks: {
+ '0.height': [2.77, 2.12],
+ '0.width': [328.1, 251.14],
+ '0.x': [455.95, 494.43],
+ '0.y': [100.54, 76.96],
+ '1.height': [13.42, 10.27],
+ '1.width': [9.95, 7.61],
+ '1.x': [647.21, 640.83],
+ '1.y': [81.82, 62.63],
+ '2.height': [7.93, 6.07],
+ '2.width': [7.74, 5.92],
+ '2.x': [604.66, 608.26],
+ '2.y': [46.74, 35.78],
+ '3.height': [30.89, 23.64],
+ '3.width': [64.23, 49.16],
+ '3.x': [520.95, 544.18],
+ '3.y': [0.1, 0.07],
+ '4.height': [51.97, 39.78],
+ '4.width': [90.05, 68.93],
+ '4.x': [476.8, 510.39],
+ '4.y': [5.22, 3.99],
+ '5.height': [14.66, 11.22],
+ '5.width': [31.35, 24],
+ '5.x': [656, 647.56],
+ '5.y': [17.42, 13.33],
+ '6.height': [9.01, 6.9],
+ '6.width': [15.29, 11.7],
+ '6.x': [664.52, 654.08],
+ '6.y': [26.89, 20.59],
+ '7.height': [10.16, 7.78],
+ '7.width': [14.26, 10.92],
+ '7.x': [750.2, 719.66],
+ '7.y': [83.76, 64.11],
+ '8.height': [19.78, 15.14],
+ '8.width': [42.58, 32.59],
+ '8.x': [708.31, 687.6],
+ '8.y': [73.78, 56.47],
+ '9.height': [36.13, 27.65],
+ '9.width': [13.77, 10.54],
+ '9.x': [559, 573.31],
+ '9.y': [107.6, 82.36],
+ '10.height': [41.61, 31.85],
+ '10.width': [14.58, 11.16],
+ '10.x': [556.23, 571.19],
+ '10.y': [102.45, 78.42],
+ '11.height': [23.08, 17.67],
+ '11.width': [17.66, 13.52],
+ '11.x': [631.29, 628.64],
+ '11.y': [74.06, 56.69],
+ '12.height': [17.09, 13.08],
+ '12.width': [8.98, 6.87],
+ '12.x': [658.04, 649.12],
+ '12.y': [65.64, 50.25],
+ '13.height': [12.73, 9.75],
+ '13.width': [7.35, 5.63],
+ '13.x': [651.48, 644.09],
+ '13.y': [73.75, 56.45],
+ '14.height': [16.89, 12.93],
+ '14.width': [14.7, 11.25],
+ '14.x': [640.14, 635.42],
+ '14.y': [53.46, 40.92],
+ '15.height': [19.59, 14.99],
+ '15.width': [9.41, 7.2],
+ '15.x': [632.39, 629.48],
+ '15.y': [51.77, 39.63],
+ '16.height': [2.29, 1.75],
+ '16.width': [2.74, 2.1],
+ '16.x': [552.53, 568.36],
+ '16.y': [55.98, 42.85],
+ '17.height': [2.78, 2.13],
+ '17.width': [3.28, 2.51],
+ '17.x': [554.86, 570.14],
+ '17.y': [56.01, 42.87],
+ '18.height': [30.9, 23.65],
+ '18.width': [242.18, 185.37],
+ '18.x': [500.34, 528.41],
+ '18.y': [0.65, 0.5],
+ '19.height': [3.95, 3.02],
+ '19.width': [1.78, 1.36],
+ '19.x': [550.35, 566.69],
+ '19.y': [47.47, 36.33],
+ '20.height': [1.14, 0.88],
+ '20.width': [2.49, 1.91],
+ '20.x': [573.83, 584.66],
+ '20.y': [139.85, 107.05],
+ '21.height': [14.2, 10.87],
+ '21.width': [16.27, 12.45],
+ '21.x': [623.42, 622.62],
+ '21.y': [0.84, 0.64],
+ '22.height': [13.38, 10.24],
+ '22.width': [35.75, 27.36],
+ '22.x': [577.32, 587.33],
+ '23.height': [1.13, 0.87],
+ '23.width': [1.68, 1.28],
+ '23.x': [672.76, 660.39],
+ '23.y': [137.43, 105.2],
+ '24.height': [1.39, 1.07],
+ '24.width': [2.26, 1.73],
+ '24.x': [735.4, 708.33],
+ '24.y': [90.96, 69.63],
+ '25.height': [14.85, 11.36],
+ '25.width': [14.63, 11.2],
+ '25.x': [634.29, 630.94],
+ '25.y': [107.91, 82.6],
+ '26.height': [2.32, 1.77],
+ '26.width': [2.09, 1.6],
+ '26.x': [643.47, 637.96],
+ '26.y': [115.68, 88.55],
+ '27.height': [21.66, 16.58],
+ '27.width': [22.97, 17.58],
+ '27.x': [519.05, 542.73],
+ '27.y': [40.88, 31.29],
+ '28.height': [5.53, 4.23],
+ '28.width': [4.52, 3.46],
+ '28.x': [569.96, 581.7],
+ '28.y': [117.38, 89.85],
+ '29.height': [47.5, 36.36],
+ '29.width': [36.3, 27.79],
+ '29.x': [551.57, 567.62],
+ '29.y': [74.07, 56.7],
+ '30.height': [16.03, 12.27],
+ '30.width': [11.89, 9.1],
+ '30.x': [555.99, 571],
+ '30.y': [92.82, 71.04],
+ '31.height': [22.68, 17.36],
+ '31.width': [12.7, 9.72],
+ '31.x': [544.51, 562.22],
+ '31.y': [80.7, 61.77],
+ '32.height': [20.87, 15.98],
+ '32.width': [11.24, 8.61],
+ '32.x': [546.65, 563.86],
+ '32.y': [65.13, 49.85],
+ '33.height': [2.97, 2.28],
+ '33.width': [5.26, 4.03],
+ '33.x': [543.32, 561.31],
+ '33.y': [68.64, 52.54],
+ '34.height': [3.72, 2.84],
+ '34.width': [2.97, 2.28],
+ '34.x': [540.87, 559.43],
+ '34.y': [66.64, 51.01],
+ '35.height': [5.3, 4.05],
+ '35.width': [4.47, 3.42],
+ '35.x': [539.56, 558.43],
+ '35.y': [61.95, 47.42],
+ '36.height': [3.72, 2.84],
+ '36.width': [5.77, 4.42],
+ '36.x': [538.26, 557.44],
+ '36.y': [60.74, 46.49],
+ '37.height': [1.57, 1.2],
+ '37.width': [2.18, 1.67],
+ '37.x': [537.47, 556.83],
+ '37.y': [62.68, 47.98],
+ '38.height': [5.01, 3.83],
+ '38.width': [3.87, 2.97],
+ '38.x': [535.66, 555.44],
+ '38.y': [58.52, 44.8],
+ '39.height': [3.19, 2.44],
+ '39.width': [1.52, 1.16],
+ '39.x': [538.64, 557.73],
+ '39.y': [57.69, 44.16],
+ '40.height': [14.25, 10.91],
+ '40.width': [12.45, 9.53],
+ '40.x': [552.33, 568.2],
+ '40.y': [65.47, 50.12],
+ '41.height': [8.86, 6.78],
+ '41.width': [4.39, 3.36],
+ '41.x': [563.11, 576.45],
+ '41.y': [70.18, 53.72],
+ '42.height': [5.26, 4.02],
+ '42.width': [3.85, 2.95],
+ '42.x': [566.15, 578.78],
+ '42.y': [73.1, 55.95],
+ '43.height': [56.7, 43.4],
+ '43.width': [58.38, 44.69],
+ '43.x': [569.38, 581.25],
+ '43.y': [21.36, 16.35],
+ '44.height': [7.93, 6.07],
+ '44.width': [5.3, 4.06],
+ '44.x': [544.83, 562.46],
+ '44.y': [78.9, 60.39],
+ '45.height': [0.7, 0.53],
+ '45.width': [1.49, 1.14],
+ '45.x': [559.06, 573.35],
+ '45.y': [57.67, 44.14],
+ '46.height': [1, 0.77],
+ '46.width': [1.88, 1.44],
+ '46.x': [548.99, 565.65],
+ '46.y': [57.67, 44.14],
+ '47.height': [4.01, 3.07],
+ '47.width': [9.34, 7.15],
+ '47.x': [543.81, 561.68],
+ '47.y': [52.04, 39.83],
+ '48.height': [8.22, 6.29],
+ '48.width': [6.96, 5.33],
+ '48.x': [642.93, 637.55],
+ '48.y': [99.91, 76.48],
+ '49.height': [11, 8.42],
+ '49.width': [8.71, 6.67],
+ '49.x': [637.66, 633.52],
+ '49.y': [102.55, 78.49],
+ '50.height': [14.48, 11.08],
+ '50.width': [12.11, 9.27],
+ '50.x': [630.66, 628.16],
+ '50.y': [101.67, 77.82],
+ '51.height': [5.25, 4.02],
+ '51.width': [5.59, 4.28],
+ '51.x': [603.89, 607.67],
+ '51.y': [60.02, 45.94],
+ '52.height': [18.13, 13.87],
+ '52.width': [15.01, 11.49],
+ '52.x': [608.87, 611.48],
+ '52.y': [49.91, 38.2],
+ '53.height': [15.4, 11.79],
+ '53.width': [10.97, 8.4],
+ '53.x': [604.66, 608.26],
+ '53.y': [47.05, 36.01],
+ '54.height': [7.57, 5.8],
+ '54.width': [2.78, 2.13],
+ '54.x': [620.71, 620.54],
+ '54.y': [65.38, 50.05],
+ '55.height': [14.4, 11.02],
+ '55.width': [14.05, 10.76],
+ '55.x': [620.27, 620.21],
+ '55.y': [51.7, 39.57],
+ '56.height': [11.96, 9.15],
+ '56.width': [10.9, 8.35],
+ '56.x': [622.49, 621.91],
+ '56.y': [63.37, 48.51],
+ '57.height': [13.86, 10.61],
+ '57.width': [6.99, 5.35],
+ '57.x': [627.87, 626.02],
+ '57.y': [64.61, 49.46],
+ '58.height': [6.33, 4.85],
+ '58.width': [1.77, 1.36],
+ '58.x': [619.96, 619.97],
+ '58.y': [66.89, 51.2],
+ '59.height': [7.95, 6.09],
+ '59.width': [3.99, 3.05],
+ '59.x': [616.99, 617.7],
+ '59.y': [66.79, 51.12],
+ '60.height': [7.7, 5.9],
+ '60.width': [5.6, 4.28],
+ '60.x': [612.03, 613.9],
+ '60.y': [67.5, 51.67],
+ '61.height': [6.55, 5.01],
+ '61.width': [6.69, 5.12],
+ '61.x': [606.07, 609.34],
+ '61.y': [64.95, 49.71],
+ '62.height': [1.96, 1.5],
+ '62.width': [2.74, 2.1],
+ '62.x': [604.68, 608.28],
+ '62.y': [64.9, 49.67],
+ '63.height': [5.22, 4],
+ '63.width': [3.6, 2.76],
+ '63.x': [609.41, 611.9],
+ '63.y': [69.96, 53.55],
+ '64.height': [4.06, 3.11],
+ '64.width': [2.78, 2.13],
+ '64.x': [607.77, 610.64],
+ '64.y': [68.09, 52.12],
+ '65.height': [6.8, 5.21],
+ '65.width': [7.03, 5.38],
+ '65.x': [614.96, 616.15],
+ '65.y': [61.83, 47.33],
+ '66.height': [11.06, 8.47],
+ '66.width': [11.97, 9.16],
+ '66.x': [633.4, 630.26],
+ '66.y': [66.73, 51.08],
+ '67.height': [10.96, 8.39],
+ '67.width': [6.83, 5.23],
+ '67.x': [630.29, 627.88],
+ '67.y': [75.97, 58.15],
+ '68.height': [7.89, 6.04],
+ '68.width': [5.23, 4],
+ '68.x': [628.17, 626.25],
+ '68.y': [77.72, 59.49],
+ '69.height': [1.59, 1.22],
+ '69.width': [1.84, 1.41],
+ '69.x': [628.64, 626.61],
+ '69.y': [77.77, 59.53],
+ '70.height': [11.99, 9.18],
+ '70.width': [10.9, 8.35],
+ '70.x': [639.95, 635.27],
+ '70.y': [90.92, 69.59],
+ '71.height': [9.34, 7.15],
+ '71.width': [2.69, 2.06],
+ '71.x': [649.94, 642.92],
+ '71.y': [92.16, 70.54],
+ '72.height': [19.94, 15.27],
+ '72.width': [9.69, 7.42],
+ '72.x': [647.59, 641.12],
+ '72.y': [93.5, 71.57],
+ '73.height': [1.92, 1.47],
+ '73.width': [1.22, 0.94],
+ '73.x': [647.03, 640.69],
+ '73.y': [112.17, 85.86],
+ '74.height': [16.69, 12.78],
+ '74.width': [11.53, 8.82],
+ '74.x': [630.59, 628.11],
+ '74.y': [86.18, 65.97],
+ '75.height': [2.69, 2.06],
+ '75.width': [1.6, 1.22],
+ '75.x': [646.94, 640.62],
+ '75.y': [83.57, 63.97],
+ '76.height': [4.34, 3.32],
+ '76.width': [1.11, 0.85],
+ '76.x': [649.6, 642.66],
+ '76.y': [40.25, 30.81],
+ '77.height': [1.76, 1.35],
+ '77.width': [1.09, 0.83],
+ '77.x': [650.07, 643.01],
+ '77.y': [38.7, 29.62],
+ '78.height': [16.46, 12.6],
+ '78.width': [7.34, 5.62],
+ '78.x': [658.75, 649.66],
+ '78.y': [95.64, 73.21],
+ '79.height': [1.35, 1.03],
+ '79.width': [0.5, 0.38],
+ '79.x': [650.12, 643.05],
+ '79.y': [41.09, 31.46],
+ '80.height': [0.92, 0.7],
+ '80.width': [2.76, 2.11],
+ '80.x': [604.55, 608.18],
+ '80.y': [63.36, 48.5],
+ '81.height': [7.97, 6.1],
+ '81.width': [3.42, 2.62],
+ '81.x': [626.41, 624.91],
+ '81.y': [35.68, 27.31],
+ '82.height': [21.08, 16.13],
+ '82.width': [18.33, 14.03],
+ '82.x': [612.37, 614.16],
+ '82.y': [35.94, 27.51],
+ '83.height': [4.81, 3.68],
+ '83.width': [3.3, 2.52],
+ '83.x': [650.41, 643.27],
+ '83.y': [40.13, 30.72],
+ '84.height': [4.24, 3.25],
+ '84.width': [4.16, 3.18],
+ '84.x': [665.87, 655.11],
+ '84.y': [48.62, 37.22],
+ '85.height': [1.85, 1.41],
+ '85.width': [0.81, 0.62],
+ '85.x': [664.92, 654.39],
+ '85.y': [48.55, 37.17],
+ '86.height': [1.78, 1.37],
+ '86.width': [1.71, 1.31],
+ '86.x': [660.62, 651.09],
+ '86.y': [43.94, 33.63],
+ '87.height': [9.41, 7.2],
+ '87.width': [9.05, 6.93],
+ '87.x': [653.16, 645.38],
+ '87.y': [35.64, 27.28],
+ '88.height': [11.73, 8.98],
+ '88.width': [6.51, 4.99],
+ '88.x': [667.03, 656],
+ '88.y': [48.22, 36.91],
+ '89.height': [2.42, 1.85],
+ '89.width': [0.71, 0.54],
+ '89.x': [772.12, 736.44],
+ '89.y': [98.83, 75.65],
+ '90.height': [5.05, 3.86],
+ '90.width': [4.91, 3.76],
+ '90.x': [713.82, 691.81],
+ '90.y': [62.5, 47.84],
+ '91.height': [18.15, 13.89],
+ '91.width': [8.27, 6.33],
+ '91.x': [708.21, 687.52],
+ '91.y': [55.37, 42.38],
+ '92.height': [10.45, 8],
+ '92.width': [8.09, 6.2],
+ '92.x': [710.18, 689.03],
+ '92.y': [52.9, 40.49],
+ '93.height': [22.29, 17.06],
+ '93.width': [8.67, 6.64],
+ '93.x': [702.87, 683.43],
+ '93.y': [45.94, 35.17],
+ '94.height': [18.05, 13.82],
+ '94.width': [8.99, 6.88],
+ '94.x': [711.43, 689.99],
+ '94.y': [51.84, 39.68],
+ '95.height': [5.73, 4.39],
+ '95.width': [4.26, 3.26],
+ '95.x': [722.34, 698.33],
+ '95.y': [29.6, 22.66],
+ '96.height': [4.69, 3.59],
+ '96.width': [4.07, 3.11],
+ '96.x': [725.28, 700.59],
+ '96.y': [34.29, 26.25],
+ '97.height': [10.56, 8.09],
+ '97.width': [26.47, 20.26],
+ '97.x': [687.61, 671.75],
+ '97.y': [20.51, 15.7],
+ '98.height': [32.93, 25.21],
+ '98.width': [24.7, 18.9],
+ '98.x': [680.75, 666.5],
+ '98.y': [37.75, 28.89],
+ '99.height': [6.9, 5.28],
+ '99.width': [5.22, 3.99],
+ '99.x': [698.01, 679.71],
+ '99.y': [48.16, 36.87],
+ '100.height': [1.85, 1.42],
+ '100.width': [2.98, 2.28],
+ '100.x': [698.06, 679.75],
+ '100.y': [45.99, 35.2],
+ '101.height': [4.7, 3.6],
+ '101.width': [7.73, 5.91],
+ '101.x': [689.95, 673.54],
+ '101.y': [43.52, 33.31],
+ '102.height': [15.51, 11.87],
+ '102.width': [12.86, 9.85],
+ '102.x': [672.93, 660.51],
+ '102.y': [35.92, 27.5],
+ '103.height': [10.36, 7.93],
+ '103.width': [11.29, 8.64],
+ '103.x': [671.66, 659.54],
+ '103.y': [34.43, 26.36],
+ '104.height': [4.61, 3.53],
+ '104.width': [6.88, 5.26],
+ '104.x': [675.81, 662.72],
+ '104.y': [31.75, 24.3],
+ '105.height': [4.3, 3.29],
+ '105.width': [7.85, 6.01],
+ '105.x': [677.22, 663.8],
+ '105.y': [29.27, 22.4],
+ '106.height': [8.15, 6.24],
+ '106.width': [12.91, 9.88],
+ '106.x': [662.73, 652.7],
+ '106.y': [29.85, 22.85],
+ '107.height': [16.69, 12.77],
+ '107.width': [19.34, 14.8],
+ '107.x': [656.44, 647.89],
+ '107.y': [33.1, 25.33],
+ '108.height': [5.53, 4.23],
+ '108.width': [5.09, 3.89],
+ '108.x': [650.36, 643.24],
+ '108.y': [35.82, 27.41],
+ '109.height': [2.71, 2.07],
+ '109.width': [2.95, 2.25],
+ '109.x': [655.63, 647.27],
+ '109.y': [31.44, 24.07],
+ '110.height': [10.81, 8.28],
+ '110.width': [7.91, 6.06],
+ '110.x': [627.8, 625.97],
+ '110.y': [6.62, 5.07],
+ '111.height': [4.49, 3.44],
+ '111.width': [6.94, 5.32],
+ '111.x': [637.4, 633.32],
+ '111.y': [16.71, 12.79],
+ '112.height': [6.96, 5.33],
+ '112.width': [13.87, 10.61],
+ '112.x': [637.13, 633.11],
+ '112.y': [20.23, 15.49],
+ '113.height': [5.53, 4.23],
+ '113.width': [7.82, 5.99],
+ '113.x': [630.48, 628.02],
+ '113.y': [17.9, 13.7],
+ '114.height': [2.61, 2],
+ '114.width': [5.77, 4.42],
+ '114.x': [627.43, 625.69],
+ '114.y': [23.42, 17.93],
+ '115.height': [2.89, 2.21],
+ '115.width': [4.97, 3.81],
+ '115.x': [632.72, 629.74],
+ '115.y': [23.83, 18.24],
+ '116.height': [3.01, 2.3],
+ '116.width': [2.97, 2.27],
+ '116.x': [640.69, 635.83],
+ '116.y': [23.99, 18.36],
+ '117.height': [4.63, 3.55],
+ '117.width': [7.61, 5.83],
+ '117.x': [635.96, 632.22],
+ '117.y': [24.23, 18.55],
+ '118.height': [2.24, 1.71],
+ '118.width': [4.18, 3.2],
+ '118.x': [635.31, 631.72],
+ '118.y': [16.53, 12.65],
+ '119.height': [2.08, 1.59],
+ '119.width': [5.23, 4],
+ '119.x': [635.22, 631.65],
+ '119.y': [15.13, 11.58],
+ '120.height': [1.82, 1.4],
+ '120.width': [3.36, 2.57],
+ '120.x': [636.46, 632.6],
+ '120.y': [13.74, 10.51],
+ '121.height': [7.37, 5.64],
+ '121.width': [6.88, 5.27],
+ '121.x': [624.52, 623.46],
+ '121.y': [17.78, 13.61],
+ '122.height': [3.17, 2.42],
+ '122.width': [4.97, 3.81],
+ '122.x': [637.97, 633.75],
+ '122.y': [28.29, 21.65],
+ '123.height': [7.57, 5.79],
+ '123.width': [5.62, 4.3],
+ '123.x': [636.63, 632.73],
+ '123.y': [30.83, 23.59],
+ '124.height': [6.89, 5.27],
+ '124.width': [16.24, 12.43],
+ '124.x': [641.25, 636.26],
+ '124.y': [30.49, 23.34],
+ '125.height': [3.28, 2.51],
+ '125.width': [1.56, 1.19],
+ '125.x': [635.67, 631.99],
+ '125.y': [29.91, 22.9],
+ '126.height': [4.17, 3.19],
+ '126.width': [4.56, 3.49],
+ '126.x': [630.85, 628.3],
+ '126.y': [25.96, 19.87],
+ '127.height': [2.08, 1.59],
+ '127.width': [3.45, 2.64],
+ '127.x': [624.75, 623.63],
+ '127.y': [24.62, 18.84],
+ '128.height': [0.67, 0.51],
+ '128.width': [0.42, 0.32],
+ '128.x': [624.36, 623.34],
+ '128.y': [22.35, 17.11],
+ '129.height': [1.88, 1.44],
+ '129.width': [2.78, 2.13],
+ '129.x': [621.91, 621.46],
+ '129.y': [21.05, 16.11],
+ '130.height': [2.56, 1.96],
+ '130.width': [2.78, 2.13],
+ '130.x': [622.51, 621.92],
+ '130.y': [19.13, 14.64],
+ '131.height': [5.9, 4.52],
+ '131.width': [2.69, 2.06],
+ '131.x': [612.1, 613.95],
+ '131.y': [30.34, 23.23],
+ '132.height': [8.44, 6.46],
+ '132.width': [10.05, 7.7],
+ '132.x': [612.42, 614.2],
+ '132.y': [28.8, 22.04],
+ '133.height': [3.22, 2.47],
+ '133.width': [3.03, 2.32],
+ '133.x': [612.47, 614.24],
+ '133.y': [17.64, 13.5],
+ '134.height': [2.77, 2.12],
+ '134.width': [1.84, 1.41],
+ '134.x': [767.86, 733.18],
+ '134.y': [105.52, 80.77],
+ '135.height': [5.26, 4.02],
+ '135.width': [4.69, 3.59],
+ '135.x': [764.86, 730.88],
+ '135.y': [88.88, 68.03],
+ '136.height': [13.1, 10.03],
+ '136.width': [17.51, 13.41],
+ '136.x': [751.5, 720.65],
+ '136.y': [122.34, 93.65],
+ '137.height': [38.41, 29.4],
+ '137.width': [37.54, 28.73],
+ '137.x': [717.9, 694.93],
+ '137.y': [93.94, 71.9],
+ '138.height': [4.8, 3.67],
+ '138.width': [1.99, 1.52],
+ '138.x': [693.66, 676.38],
+ '138.y': [68.37, 52.33],
+ '139.height': [38.88, 29.76],
+ '139.width': [45.73, 35.01],
+ '139.x': [680.81, 666.54],
+ '139.y': [19.18, 14.68],
+ '140.height': [3.97, 3.04],
+ '140.width': [1.24, 0.95],
+ '140.x': [727.08, 701.96],
+ '140.y': [49.52, 37.91],
+ '141.height': [11.15, 8.54],
+ '141.width': [9.83, 7.52],
+ '141.x': [625.37, 624.11],
+ '141.y': [25.34, 19.4],
+ '142.height': [2.61, 2],
+ '142.width': [3.41, 2.61],
+ '142.x': [625.85, 624.48],
+ '142.y': [15.34, 11.74],
+ '143.height': [7.95, 6.09],
+ '143.width': [6.86, 5.25],
+ '143.x': [614.4, 615.71],
+ '143.y': [14.56, 11.15],
+ '144.height': [2.24, 1.71],
+ '144.width': [7.03, 5.38],
+ '144.x': [603.93, 607.7],
+ '144.y': [8.38, 6.41],
+ '145.height': [3.88, 2.97],
+ '145.width': [4.69, 3.59],
+ '145.x': [656.73, 648.11],
+ '145.y': [30.79, 23.57],
+ '146.height': [2.64, 2.02],
+ '146.width': [5.95, 4.55],
+ '146.x': [652.16, 644.61],
+ '146.y': [29, 22.2],
+ '147.height': [15.97, 12.22],
+ '147.width': [8.81, 6.74],
+ '147.x': [728.28, 702.88],
+ '147.y': [57.69, 44.16],
+ '148.height': [7.69, 5.89],
+ '148.width': [17.78, 13.61],
+ '148.x': [712.67, 690.94],
+ '148.y': [71.97, 55.09],
+ '149.height': [1.8, 1.38],
+ '149.width': [1.08, 0.83],
+ '149.x': [725.91, 701.07],
+ '149.y': [73.82, 56.5],
+ '150.height': [1.43, 1.09],
+ '150.width': [2.22, 1.7],
+ '150.x': [630.82, 628.28],
+ '150.y': [25.6, 19.6],
+ '151.height': [7.59, 5.81],
+ '151.width': [8.28, 6.34],
+ '151.x': [633.14, 630.05],
+ '151.y': [5.95, 4.55],
+ '152.height': [1.8, 1.38],
+ '152.width': [4.32, 3.31],
+ '152.x': [633.09, 630.02],
+ '152.y': [22.89, 17.52],
+ '153.height': [2.5, 1.92],
+ '153.width': [5.15, 3.94],
+ '153.x': [629.36, 627.16],
+ '153.y': [21.39, 16.38],
+ '154.height': [6.8, 5.21],
+ '154.width': [6.35, 4.86],
+ '154.x': [653.2, 645.41],
+ '154.y': [58.31, 44.63],
+ '155.height': [15.89, 12.16],
+ '155.width': [8.12, 6.21],
+ '155.x': [730.65, 704.7],
+ '155.y': [26.93, 20.61],
+ '156.height': [9.81, 7.51],
+ '156.width': [7.91, 6.05],
+ '156.x': [563.86, 577.03],
+ '156.y': [104.59, 80.06],
+ '157.height': [7.86, 6.02],
+ '157.width': [9.43, 7.22],
+ '157.x': [658.91, 649.78],
+ '157.y': [57.09, 43.7],
+ '158.height': [18.8, 14.39],
+ '158.width': [19.55, 14.96],
+ '158.x': [650.34, 643.23],
+ '158.y': [41.52, 31.78],
+ '159.height': [11.53, 8.82],
+ '159.width': [206.24, 157.86],
+ '159.x': [519.76, 543.27],
+ '159.y': [150.47, 115.18],
+ '160.height': [0.75, 0.57],
+ '160.width': [1.48, 1.13],
+ '160.x': [647.72, 641.22],
+ '160.y': [37.55, 28.74],
+ '161.height': [0.68, 0.52],
+ '161.width': [1.5, 1.15],
+ '161.x': [647.32, 640.91],
+ '161.y': [38.11, 29.17],
+ '162.height': [16.71, 12.79],
+ '162.width': [14.32, 10.96],
+ '162.x': [604.72, 608.3],
+ '162.y': [37.45, 28.67],
+ '163.height': [11.28, 8.63],
+ '163.width': [11.6, 8.88],
+ '163.x': [641.44, 636.41],
+ '163.y': [42.18, 32.29],
+ '164.height': [15.98, 12.23],
+ '164.width': [14.32, 10.96],
+ '164.x': [628.22, 626.3],
+ '164.y': [40.41, 30.93],
+ '165.height': [14.33, 10.97],
+ '165.width': [13.71, 10.49],
+ '165.x': [650.47, 643.32],
+ '165.y': [62.02, 47.48],
+ '166.height': [2.19, 1.68],
+ '166.width': [1.47, 1.13],
+ '166.x': [658.31, 649.33],
+ '166.y': [64.81, 49.61],
+ '167.height': [4.3, 3.29],
+ '167.width': [5.95, 4.55],
+ '167.x': [659.21, 650.01],
+ '167.y': [66.34, 50.78],
+ '168.height': [7.12, 5.45],
+ '168.width': [5.06, 3.88],
+ '168.x': [647.47, 641.03],
+ '168.y': [75.32, 57.65],
+ '169.height': [2.23, 1.71],
+ '169.width': [1.67, 1.28],
+ '169.x': [646.94, 640.62],
+ '169.y': [82.05, 62.81],
+ '170.height': [2.7, 2.06],
+ '170.width': [3.16, 2.42],
+ '170.x': [632.55, 629.61],
+ '170.y': [27.26, 20.86],
+ '171.height': [1.58, 1.21],
+ '171.width': [2.05, 1.57],
+ '171.x': [636.68, 632.77],
+ '171.y': [30.3, 23.19],
+ '172.height': [4.09, 3.13],
+ '172.width': [3.64, 2.78],
+ '172.x': [634.89, 631.4],
+ '172.y': [26.3, 20.13],
+ '173.height': [1.74, 1.33],
+ '173.width': [1.48, 1.14],
+ '173.x': [634.94, 631.44],
+ '173.y': [29.03, 22.22],
+ '174.height': [1.51, 1.15],
+ '174.width': [1.37, 1.05],
+ '174.x': [636.25, 632.44],
+ '174.y': [29.3, 22.42],
+ '175.height': [1.1, 0.85],
+ '175.width': [1.04, 0.79],
+ '175.x': [562.89, 576.28],
+ '175.y': [67.05, 51.32],
+ '176.height': [10.87, 8.32],
+ '176.width': [10.64, 8.15],
+ '176.x': [642.07, 636.89],
+ '176.y': [65.37, 50.03],
+ },
+ legend: {
+ 'continuousLegend.bounds.height': [61, 99],
+ 'continuousLegend.bar.y': [107.2, 122.2],
+ },
+ },
+ style: {
+ 'legends.continuousLegend.title': ['gdp ($)', 'gdp'],
+ },
+ },
+ '../gallery/maps.stories.tsx.worldMercatorSpec @390': {
+ reason:
+ 'Approved B3 static map captions and neutral-class context reserve measured key space; occupied-bounds size-key placement has no legacy layout opt-out.',
+ changes: {
+ dims: {
+ 'mapSize.height': [166.7, 128.7],
+ },
+ area: {
+ 'rect.height': [166.7, 128.7],
+ 'rect.y': [157.3, 195.3],
+ },
+ marks: {
+ '0.height': [1.7, 1.31],
+ '0.width': [257.72, 198.97],
+ '0.x': [56.14, 85.51],
+ '0.y': [130.21, 100.53],
+ '1.height': [7.77, 6],
+ '1.width': [7.86, 6.07],
+ '1.x': [206, 201.22],
+ '1.y': [119.27, 92.08],
+ '2.height': [5.23, 4.04],
+ '2.width': [6.01, 4.64],
+ '2.x': [172.79, 175.57],
+ '2.y': [97.97, 75.64],
+ '3.height': [83.13, 64.18],
+ '3.width': [63.25, 48.83],
+ '3.x': [84.06, 107.07],
+ '3.y': [2.58, 1.99],
+ '4.height': [60.32, 46.57],
+ '4.width': [75.04, 57.94],
+ '4.x': [62.02, 90.05],
+ '4.y': [44.47, 34.33],
+ '5.height': [15.91, 12.28],
+ '5.width': [29.27, 22.6],
+ '5.x': [218.27, 210.68],
+ '5.y': [70.76, 54.63],
+ '6.height': [8.07, 6.23],
+ '6.width': [12.26, 9.47],
+ '6.x': [225.04, 215.91],
+ '6.y': [81.84, 63.18],
+ '7.height': [5.88, 4.54],
+ '7.width': [10.75, 8.3],
+ '7.x': [285.94, 262.93],
+ '7.y': [120.38, 92.94],
+ '8.height': [11.39, 8.79],
+ '8.width': [32.74, 25.28],
+ '8.x': [253.22, 237.67],
+ '8.y': [114.66, 88.52],
+ '9.height': [31.63, 24.42],
+ '9.width': [14.16, 10.94],
+ '9.x': [132.44, 144.42],
+ '9.y': [134.61, 103.92],
+ '10.height': [35.33, 27.27],
+ '10.width': [6.22, 4.8],
+ '10.x': [130.85, 143.19],
+ '10.y': [131.37, 101.43],
+ '11.height': [13.34, 10.3],
+ '11.width': [13.6, 10.5],
+ '11.x': [193.72, 191.73],
+ '11.y': [114.82, 88.65],
+ '12.height': [9.88, 7.63],
+ '12.width': [7.27, 5.61],
+ '12.x': [214.34, 207.65],
+ '12.y': [109.91, 84.86],
+ '13.height': [7.3, 5.64],
+ '13.width': [5.7, 4.4],
+ '13.x': [209.26, 203.73],
+ '13.y': [114.64, 88.51],
+ '14.height': [10.24, 7.91],
+ '14.width': [11.79, 9.11],
+ '14.x': [200.7, 197.12],
+ '14.y': [102.43, 79.08],
+ '15.height': [11.92, 9.2],
+ '15.width': [7.41, 5.72],
+ '15.x': [194.69, 192.48],
+ '15.y': [101.34, 78.24],
+ '16.height': [1.43, 1.1],
+ '16.width': [2.03, 1.57],
+ '16.x': [131.7, 143.85],
+ '16.y': [104.03, 80.32],
+ '17.height': [1.73, 1.33],
+ '17.width': [2.6, 2],
+ '17.x': [133.5, 145.24],
+ '17.y': [104.06, 80.34],
+ '18.height': [73.05, 56.4],
+ '18.width': [257.72, 198.97],
+ '18.x': [56.14, 85.51],
+ '18.y': [13.16, 10.16],
+ '19.height': [2.64, 2.04],
+ '19.width': [1.42, 1.09],
+ '19.x': [128.46, 141.35],
+ '19.y': [98.47, 76.02],
+ '20.height': [1.39, 1.07],
+ '20.width': [2.47, 1.91],
+ '20.x': [141.19, 151.18],
+ '20.y': [161.28, 124.52],
+ '21.height': [51.39, 39.67],
+ '21.width': [18.83, 14.54],
+ '21.x': [188.57, 187.76],
+ '21.y': [15.85, 12.24],
+ '22.height': [64.51, 49.81],
+ '22.width': [43.73, 33.76],
+ '22.x': [132.53, 144.49],
+ '23.height': [1.26, 0.97],
+ '23.width': [1.32, 1.02],
+ '23.x': [234.2, 222.98],
+ '23.y': [158.53, 122.39],
+ '24.height': [0.81, 0.63],
+ '24.width': [1.69, 1.31],
+ '24.x': [274.46, 254.07],
+ '24.y': [124.53, 96.14],
+ '25.height': [10.4, 8.03],
+ '25.width': [11.8, 9.11],
+ '25.x': [196.7, 194.03],
+ '25.y': [134.81, 104.08],
+ '26.height': [1.65, 1.27],
+ '26.width': [1.66, 1.29],
+ '26.x': [204.33, 199.92],
+ '26.y': [140.01, 108.09],
+ '27.height': [14.29, 11.03],
+ '27.width': [21.7, 16.76],
+ '27.x': [101.15, 120.26],
+ '27.y': [93.77, 72.4],
+ '28.height': [4.11, 3.18],
+ '28.width': [3.73, 2.88],
+ '28.x': [143.17, 152.71],
+ '28.y': [141.21, 109.02],
+ '29.height': [29.47, 22.75],
+ '29.width': [28.1, 21.7],
+ '29.x': [132.03, 144.11],
+ '29.y': [114.83, 88.65],
+ '30.height': [9.8, 7.57],
+ '30.width': [8.66, 6.68],
+ '30.x': [135.18, 146.54],
+ '30.y': [125.61, 96.98],
+ '31.height': [13.33, 10.29],
+ '31.width': [9.12, 7.04],
+ '31.x': [126.72, 140.01],
+ '31.y': [118.63, 91.59],
+ '32.height': [12.05, 9.31],
+ '32.width': [8.67, 6.7],
+ '32.x': [128.45, 141.34],
+ '32.y': [109.61, 84.63],
+ '33.height': [1.73, 1.34],
+ '33.width': [4.1, 3.16],
+ '33.x': [125.61, 139.15],
+ '33.y': [111.67, 86.22],
+ '34.height': [2.17, 1.68],
+ '34.width': [2.43, 1.88],
+ '34.x': [123.48, 137.5],
+ '34.y': [110.5, 85.31],
+ '35.height': [3.15, 2.43],
+ '35.width': [3.24, 2.5],
+ '35.x': [122.24, 136.54],
+ '35.y': [107.71, 83.16],
+ '36.height': [2.23, 1.72],
+ '36.width': [4.44, 3.43],
+ '36.x': [121.03, 135.61],
+ '36.y': [106.98, 82.59],
+ '37.height': [0.94, 0.73],
+ '37.width': [1.7, 1.31],
+ '37.x': [120.5, 135.2],
+ '37.y': [108.15, 83.5],
+ '38.height': [3.04, 2.35],
+ '38.width': [2.87, 2.21],
+ '38.x': [118.98, 134.03],
+ '38.y': [105.62, 81.54],
+ '39.height': [1.96, 1.51],
+ '39.width': [0.8, 0.62],
+ '39.x': [121.12, 135.68],
+ '39.y': [105.11, 81.15],
+ '40.height': [8.25, 6.37],
+ '40.width': [9.7, 7.49],
+ '40.x': [132.52, 144.48],
+ '40.y': [109.81, 84.78],
+ '41.height': [5.1, 3.94],
+ '41.width': [3.49, 2.69],
+ '41.x': [141.04, 151.06],
+ '41.y': [112.58, 86.91],
+ '42.height': [3.02, 2.33],
+ '42.width': [2.93, 2.26],
+ '42.x': [143.45, 152.92],
+ '42.y': [114.26, 88.22],
+ '43.height': [41.28, 31.87],
+ '43.width': [45.88, 35.42],
+ '43.x': [145.97, 154.86],
+ '43.y': [75.84, 58.55],
+ '44.height': [4.54, 3.51],
+ '44.width': [4.1, 3.17],
+ '44.x': [127.04, 140.25],
+ '44.y': [117.6, 90.79],
+ '45.height': [0.43, 0.33],
+ '45.width': [1.18, 0.91],
+ '45.x': [136.86, 147.83],
+ '45.y': [105.09, 81.13],
+ '46.height': [0.62, 0.48],
+ '46.width': [1.53, 1.18],
+ '46.x': [128.92, 141.7],
+ '46.y': [105.09, 81.13],
+ '47.height': [2.57, 1.98],
+ '47.width': [7.73, 5.97],
+ '47.x': [124.17, 138.04],
+ '47.y': [101.51, 78.37],
+ '48.height': [5.12, 3.95],
+ '48.width': [5.43, 4.19],
+ '48.x': [203.09, 198.96],
+ '48.y': [129.83, 100.23],
+ '49.height': [7.1, 5.48],
+ '49.width': [6.83, 5.27],
+ '49.x': [199.24, 196],
+ '49.y': [131.44, 101.47],
+ '50.height': [9.44, 7.29],
+ '50.width': [9.56, 7.38],
+ '50.x': [193.4, 191.49],
+ '50.y': [130.9, 101.06],
+ '51.height': [3.15, 2.44],
+ '51.width': [4.41, 3.4],
+ '51.x': [172.38, 175.26],
+ '51.y': [106.53, 82.25],
+ '52.height': [11.21, 8.65],
+ '52.width': [11.77, 9.09],
+ '52.x': [176.29, 178.27],
+ '52.y': [100.11, 77.29],
+ '53.height': [9.83, 7.59],
+ '53.width': [8.69, 6.71],
+ '53.x': [172.79, 175.57],
+ '53.y': [98.18, 75.8],
+ '54.height': [4.42, 3.41],
+ '54.width': [2.16, 1.67],
+ '54.x': [185.55, 185.43],
+ '54.y': [109.76, 84.74],
+ '55.height': [8.89, 6.86],
+ '55.width': [11.17, 8.63],
+ '55.x': [185.21, 185.16],
+ '55.y': [101.29, 78.2],
+ '56.height': [6.99, 5.39],
+ '56.width': [8.51, 6.57],
+ '56.x': [186.93, 186.49],
+ '56.y': [108.56, 83.81],
+ '57.height': [8.05, 6.21],
+ '57.width': [5.39, 4.16],
+ '57.x': [191.08, 189.69],
+ '57.y': [109.3, 84.39],
+ '58.height': [3.69, 2.85],
+ '58.width': [1.37, 1.06],
+ '58.y': [110.65, 85.43],
+ '59.height': [4.62, 3.57],
+ '59.width': [3.08, 2.38],
+ '59.x': [182.68, 183.21],
+ '59.y': [110.59, 85.38],
+ '60.height': [4.47, 3.45],
+ '60.width': [4.32, 3.34],
+ '60.x': [178.84, 180.25],
+ '60.y': [111.01, 85.7],
+ '61.height': [3.84, 2.96],
+ '61.width': [5.22, 4.03],
+ '61.x': [174.17, 176.64],
+ '61.y': [109.5, 84.54],
+ '62.height': [1.16, 0.9],
+ '62.width': [2.13, 1.65],
+ '62.x': [173.06, 175.78],
+ '62.y': [109.47, 84.52],
+ '63.height': [3.02, 2.33],
+ '63.width': [2.79, 2.15],
+ '63.x': [176.81, 178.68],
+ '63.y': [112.45, 86.82],
+ '64.height': [2.36, 1.82],
+ '64.width': [2.16, 1.67],
+ '64.x': [175.52, 177.68],
+ '64.y': [111.36, 85.97],
+ '65.height': [4.04, 3.12],
+ '65.width': [5.47, 4.23],
+ '65.x': [181.08, 181.98],
+ '65.y': [107.64, 83.1],
+ '66.height': [6.4, 4.94],
+ '66.width': [9.24, 7.14],
+ '66.x': [195.35, 192.99],
+ '66.y': [110.56, 85.36],
+ '67.height': [6.28, 4.85],
+ '67.width': [5.27, 4.07],
+ '67.x': [192.94, 191.13],
+ '67.y': [115.92, 89.49],
+ '68.height': [4.52, 3.49],
+ '68.width': [4.03, 3.11],
+ '68.x': [191.3, 189.86],
+ '68.y': [116.92, 90.27],
+ '69.height': [0.91, 0.7],
+ '69.width': [1.42, 1.09],
+ '69.x': [191.66, 190.14],
+ '69.y': [116.95, 90.29],
+ '70.height': [7.16, 5.52],
+ '70.width': [8.3, 6.41],
+ '70.x': [200.67, 197.1],
+ '70.y': [124.51, 96.12],
+ '71.height': [5.57, 4.3],
+ '71.width': [2.21, 1.71],
+ '71.x': [208.4, 203.07],
+ '71.y': [125.22, 96.68],
+ '72.height': [12.45, 9.61],
+ '72.width': [7.58, 5.86],
+ '72.x': [206.61, 201.68],
+ '72.y': [126.01, 97.29],
+ '73.height': [1.3, 1],
+ '73.width': [1, 0.77],
+ '73.x': [206.96, 201.96],
+ '73.y': [137.6, 106.24],
+ '74.height': [9.87, 7.62],
+ '74.width': [8.9, 6.87],
+ '74.x': [193.33, 191.43],
+ '74.y': [121.77, 94.01],
+ '75.height': [1.54, 1.19],
+ '75.width': [1.24, 0.96],
+ '75.x': [205.78, 201.04],
+ '75.y': [120.27, 92.85],
+ '76.height': [3.17, 2.45],
+ '76.width': [1.12, 0.87],
+ '76.x': [209.53, 203.94],
+ '76.y': [93.3, 72.03],
+ '77.height': [1.34, 1.04],
+ '77.width': [1.06, 0.82],
+ '77.x': [210.15, 204.41],
+ '77.y': [92.12, 71.12],
+ '78.height': [10.29, 7.94],
+ '78.width': [5.17, 3.99],
+ '78.x': [215.96, 208.91],
+ '78.y': [127.27, 98.26],
+ '79.height': [1, 0.77],
+ '79.width': [0.44, 0.34],
+ '79.x': [210, 204.3],
+ '79.y': [93.93, 72.52],
+ '80.height': [0.55, 0.42],
+ '80.width': [2.15, 1.66],
+ '80.x': [172.94, 175.69],
+ '80.y': [108.55, 83.81],
+ '81.height': [6.08, 4.69],
+ '81.width': [2.84, 2.19],
+ '81.x': [190.39, 189.16],
+ '81.y': [89.72, 69.27],
+ '82.height': [14.75, 11.39],
+ '82.width': [14.81, 11.43],
+ '82.x': [178.78, 180.2],
+ '82.y': [89.93, 69.43],
+ '83.height': [3.5, 2.71],
+ '83.width': [3.06, 2.36],
+ '83.x': [210, 204.3],
+ '83.y': [93.21, 71.96],
+ '84.height': [2.8, 2.16],
+ '84.width': [3.45, 2.66],
+ '84.x': [221.93, 213.51],
+ '84.y': [99.26, 76.63],
+ '85.height': [1.23, 0.95],
+ '85.width': [0.62, 0.48],
+ '85.x': [221.33, 213.05],
+ '85.y': [99.21, 76.59],
+ '86.height': [1.26, 0.97],
+ '86.width': [1.32, 1.02],
+ '86.x': [218.34, 210.74],
+ '86.y': [96.01, 74.12],
+ '87.height': [7.1, 5.49],
+ '87.width': [7, 5.4],
+ '87.x': [212.77, 206.44],
+ '87.y': [89.69, 69.25],
+ '88.height': [7.51, 5.8],
+ '88.width': [5.59, 4.32],
+ '88.x': [222.23, 213.74],
+ '88.y': [98.98, 76.42],
+ '89.height': [1.47, 1.13],
+ '89.width': [0.87, 0.67],
+ '89.x': [304.29, 277.1],
+ '89.y': [129.17, 99.73],
+ '90.height': [3, 2.31],
+ '90.width': [3.77, 2.91],
+ '90.x': [258.27, 241.57],
+ '90.y': [108.04, 83.41],
+ '91.height': [10.86, 8.38],
+ '91.width': [5.88, 4.54],
+ '91.x': [254.71, 238.82],
+ '91.y': [103.65, 80.02],
+ '92.height': [6.47, 5],
+ '92.width': [5.33, 4.12],
+ '92.x': [256.67, 240.33],
+ '92.y': [102.08, 78.81],
+ '93.height': [14.02, 10.82],
+ '93.width': [6.36, 4.91],
+ '93.x': [251.08, 236.02],
+ '93.y': [97.42, 75.21],
+ '94.height': [11.02, 8.51],
+ '94.width': [5.13, 3.96],
+ '94.x': [258.14, 241.47],
+ '94.y': [101.39, 78.27],
+ '95.height': [5, 3.86],
+ '95.width': [4.66, 3.6],
+ '95.x': [273.96, 253.68],
+ '95.y': [84.44, 65.19],
+ '96.height': [3.76, 2.9],
+ '96.width': [2.4, 1.85],
+ '96.x': [275.29, 254.71],
+ '96.y': [88.58, 68.39],
+ '97.height': [10.98, 8.48],
+ '97.width': [22.92, 17.7],
+ '97.x': [247.82, 233.5],
+ '97.y': [74.8, 57.75],
+ '98.height': [21.49, 16.59],
+ '98.width': [20.92, 16.15],
+ '98.x': [233.81, 222.68],
+ '98.y': [91.38, 70.55],
+ '99.height': [4.51, 3.49],
+ '99.width': [3.29, 2.54],
+ '99.x': [248.06, 233.68],
+ '99.y': [98.94, 76.39],
+ '100.height': [1.27, 0.98],
+ '100.width': [2.36, 1.82],
+ '100.x': [248.58, 234.09],
+ '100.y': [97.45, 75.24],
+ '101.height': [3.28, 2.53],
+ '101.width': [5.79, 4.47],
+ '101.x': [242.33, 229.26],
+ '101.y': [95.71, 73.89],
+ '102.height': [11.2, 8.65],
+ '102.width': [12.14, 9.38],
+ '102.x': [228.58, 218.65],
+ '102.y': [89.92, 69.42],
+ '103.height': [7.92, 6.12],
+ '103.width': [10.47, 8.09],
+ '103.x': [228.33, 218.45],
+ '103.y': [88.69, 68.48],
+ '104.height': [3.88, 3],
+ '104.width': [5.4, 4.17],
+ '104.x': [233.28, 222.27],
+ '104.y': [86.39, 66.7],
+ '105.height': [3.83, 2.96],
+ '105.width': [7.73, 5.97],
+ '105.x': [234.73, 223.39],
+ '105.y': [84.13, 64.95],
+ '106.height': [6.9, 5.33],
+ '106.width': [10.05, 7.76],
+ '106.x': [222.59, 214.02],
+ '106.y': [84.67, 65.37],
+ '107.height': [12.47, 9.63],
+ '107.width': [13.75, 10.62],
+ '107.x': [216.58, 209.38],
+ '107.y': [87.56, 67.6],
+ '108.height': [4.29, 3.31],
+ '108.width': [4.76, 3.68],
+ '108.x': [210.56, 204.73],
+ '108.y': [89.83, 69.35],
+ '109.height': [2.34, 1.81],
+ '109.width': [2.09, 1.62],
+ '109.x': [216.2, 209.09],
+ '109.y': [86.12, 66.49],
+ '110.height': [21.55, 16.64],
+ '110.width': [9.22, 7.12],
+ '110.x': [192.9, 191.1],
+ '110.y': [49.24, 38.01],
+ '111.height': [5.88, 4.54],
+ '111.width': [6.8, 5.25],
+ '111.x': [201.61, 197.82],
+ '111.y': [69.76, 53.86],
+ '112.height': [7.67, 5.92],
+ '112.width': [12.88, 9.95],
+ '112.x': [200.81, 197.21],
+ '112.y': [74.47, 57.49],
+ '113.height': [6.78, 5.23],
+ '113.width': [7.13, 5.5],
+ '113.x': [195.08, 192.78],
+ '113.y': [71.43, 55.15],
+ '114.height': [2.78, 2.14],
+ '114.width': [5.37, 4.14],
+ '114.x': [191.79, 190.24],
+ '114.y': [78.19, 60.37],
+ '115.height': [3.02, 2.33],
+ '115.width': [4.66, 3.6],
+ '115.x': [196.6, 193.95],
+ '115.y': [78.64, 60.71],
+ '116.height': [3.13, 2.41],
+ '116.width': [2.44, 1.88],
+ '116.x': [204.06, 199.71],
+ '116.y': [78.81, 60.85],
+ '117.height': [4.67, 3.61],
+ '117.width': [6.73, 5.2],
+ '117.x': [199.48, 196.18],
+ '117.y': [79.08, 61.05],
+ '118.height': [3.09, 2.39],
+ '118.width': [3.96, 3.06],
+ '118.x': [200.07, 196.64],
+ '118.y': [69.5, 53.66],
+ '119.height': [3.08, 2.38],
+ '119.width': [5.1, 3.94],
+ '119.x': [200.07, 196.64],
+ '119.y': [67.39, 52.03],
+ '120.height': [2.93, 2.26],
+ '120.width': [3.43, 2.65],
+ '120.x': [201.71, 197.9],
+ '120.y': [65.12, 50.28],
+ '121.height': [8.79, 6.79],
+ '121.width': [6.46, 4.99],
+ '121.x': [189.29, 188.31],
+ '121.y': [71.26, 55.02],
+ '122.height': [2.93, 2.26],
+ '122.width': [4.42, 3.41],
+ '122.x': [201.02, 197.37],
+ '122.y': [83.2, 64.24],
+ '123.height': [6.32, 4.88],
+ '123.width': [4.62, 3.57],
+ '123.x': [199.43, 196.14],
+ '123.y': [85.56, 66.06],
+ '124.height': [5.83, 4.5],
+ '124.width': [13.42, 10.36],
+ '124.x': [203.64, 199.39],
+ '124.y': [85.26, 65.82],
+ '125.height': [2.91, 2.25],
+ '125.width': [1.23, 0.95],
+ '125.x': [198.82, 195.67],
+ '125.y': [84.73, 65.42],
+ '126.height': [4.04, 3.12],
+ '126.width': [4.11, 3.17],
+ '126.x': [194.78, 192.55],
+ '126.y': [80.89, 62.45],
+ '127.height': [2.15, 1.66],
+ '127.width': [3.16, 2.44],
+ '127.x': [189.31, 188.33],
+ '127.y': [79.49, 61.37],
+ '128.height': [0.76, 0.59],
+ '128.width': [0.41, 0.31],
+ '128.x': [189.06, 188.14],
+ '128.y': [76.99, 59.44],
+ '129.height': [2.19, 1.69],
+ '129.width': [2.61, 2.01],
+ '129.x': [186.8, 186.39],
+ '129.y': [75.46, 58.26],
+ '130.height': [3.16, 2.44],
+ '130.width': [2.7, 2.09],
+ '130.x': [187.37, 186.83],
+ '130.y': [73.07, 56.41],
+ '131.height': [5.06, 3.9],
+ '131.width': [2.25, 1.74],
+ '131.x': [178.18, 179.73],
+ '131.y': [85.13, 65.72],
+ '132.height': [7.29, 5.63],
+ '132.width': [8.9, 6.87],
+ '132.x': [178.27, 179.81],
+ '132.y': [83.69, 64.61],
+ '133.height': [4.16, 3.21],
+ '133.width': [2.82, 2.18],
+ '133.x': [177.86, 179.49],
+ '133.y': [71.08, 54.88],
+ '134.height': [1.76, 1.36],
+ '134.width': [2.21, 1.71],
+ '134.x': [302.43, 275.66],
+ '134.y': [133.28, 102.9],
+ '135.height': [3.06, 2.36],
+ '135.width': [4.23, 3.27],
+ '135.x': [297.03, 271.49],
+ '135.y': [123.32, 95.21],
+ '136.height': [11.54, 8.91],
+ '136.width': [8.6, 6.64],
+ '136.x': [304.2, 277.03],
+ '136.y': [144.89, 111.86],
+ '137.height': [27.1, 20.93],
+ '137.width': [28.8, 22.24],
+ '137.x': [266.14, 247.64],
+ '137.y': [126.27, 97.48],
+ '138.height': [2.79, 2.15],
+ '138.width': [1.5, 1.16],
+ '138.x': [242.05, 229.05],
+ '138.y': [111.52, 86.1],
+ '139.height': [32.2, 24.86],
+ '139.width': [43.92, 33.91],
+ '139.x': [237.74, 225.72],
+ '139.y': [73.13, 56.46],
+ '140.height': [2.6, 2.01],
+ '140.width': [1.32, 1.02],
+ '140.x': [270.98, 251.38],
+ '140.y': [99.86, 77.1],
+ '141.height': [10.13, 7.82],
+ '141.width': [8.4, 6.48],
+ '141.x': [189.83, 188.73],
+ '141.y': [80.25, 61.96],
+ '142.height': [3.78, 2.92],
+ '142.width': [3.29, 2.54],
+ '142.x': [190.79, 189.47],
+ '142.y': [67.71, 52.28],
+ '143.height': [10.69, 8.25],
+ '143.width': [6.63, 5.12],
+ '143.x': [179.58, 180.81],
+ '143.y': [66.48, 51.33],
+ '144.height': [5.14, 3.97],
+ '144.width': [7.67, 5.92],
+ '144.x': [167.58, 171.55],
+ '144.y': [54.13, 41.79],
+ '145.height': [3.36, 2.59],
+ '145.width': [4.01, 3.09],
+ '145.x': [217.07, 209.76],
+ '145.y': [85.53, 66.03],
+ '146.height': [2.41, 1.86],
+ '146.width': [4.78, 3.69],
+ '146.x': [213.6, 207.08],
+ '146.y': [83.88, 64.76],
+ '147.height': [9.48, 7.32],
+ '147.width': [6.7, 5.17],
+ '147.x': [268.88, 249.76],
+ '147.y': [105.1, 81.14],
+ '148.height': [4.42, 3.41],
+ '148.width': [13.67, 10.55],
+ '148.x': [256.65, 240.32],
+ '148.y': [113.61, 87.72],
+ '149.height': [1.03, 0.8],
+ '149.width': [0.89, 0.69],
+ '149.x': [266.76, 248.12],
+ '149.y': [114.68, 88.54],
+ '150.height': [1.45, 1.12],
+ '150.width': [2.05, 1.59],
+ '150.x': [194.81, 192.57],
+ '150.y': [80.53, 62.17],
+ '151.height': [17.73, 13.69],
+ '151.width': [7.78, 6.01],
+ '151.x': [199.78, 196.41],
+ '151.y': [47.06, 36.33],
+ '152.height': [1.97, 1.52],
+ '152.width': [4.07, 3.14],
+ '152.x': [197.08, 194.33],
+ '152.y': [77.61, 59.91],
+ '153.height': [2.85, 2.2],
+ '153.width': [4.73, 3.66],
+ '153.x': [193.76, 191.76],
+ '153.y': [75.87, 58.58],
+ '154.height': [4.11, 3.18],
+ '154.width': [4.84, 3.73],
+ '154.x': [211, 205.08],
+ '154.y': [105.49, 81.44],
+ '155.height': [13.32, 10.29],
+ '155.width': [11.55, 8.92],
+ '155.x': [277.64, 256.52],
+ '155.y': [81.87, 63.21],
+ '156.height': [6.41, 4.95],
+ '156.width': [6.01, 4.64],
+ '156.x': [140.12, 150.35],
+ '156.y': [132.7, 102.45],
+ '157.height': [4.77, 3.69],
+ '157.width': [7.52, 5.81],
+ '157.x': [215.5, 208.55],
+ '157.y': [104.73, 80.86],
+ '158.height': [12.47, 9.63],
+ '158.width': [15.06, 11.63],
+ '158.x': [209.79, 204.14],
+ '158.y': [94.25, 72.76],
+ '159.height': [69.91, 53.97],
+ '159.width': [257.72, 198.97],
+ '159.x': [56.14, 85.51],
+ '159.y': [177.54, 137.07],
+ '160.height': [0.59, 0.45],
+ '160.width': [1.32, 1.02],
+ '160.x': [208.43, 203.09],
+ '160.y': [91.22, 70.43],
+ '161.height': [0.53, 0.41],
+ '161.width': [1.25, 0.97],
+ '161.x': [208.09, 202.83],
+ '161.y': [91.66, 70.76],
+ '162.height': [11.74, 9.06],
+ '162.width': [11.38, 8.78],
+ '162.x': [172.82, 175.59],
+ '162.y': [91.14, 70.37],
+ '163.height': [7.7, 5.95],
+ '163.width': [8.71, 6.72],
+ '163.x': [202.68, 198.65],
+ '163.y': [94.73, 73.14],
+ '164.height': [10.87, 8.39],
+ '164.width': [11.34, 8.76],
+ '164.x': [191.67, 190.15],
+ '164.y': [93.42, 72.12],
+ '165.height': [8.38, 6.47],
+ '165.width': [10.62, 8.2],
+ '165.x': [208.59, 203.21],
+ '165.y': [107.75, 83.19],
+ '166.height': [1.3, 1],
+ '166.width': [1.19, 0.92],
+ '166.x': [214.82, 208.03],
+ '166.y': [109.42, 84.48],
+ '167.height': [2.52, 1.94],
+ '167.width': [4.57, 3.53],
+ '167.x': [215.47, 208.52],
+ '167.y': [110.33, 85.18],
+ '168.height': [4.08, 3.15],
+ '168.width': [3.91, 3.02],
+ '168.x': [206.18, 201.35],
+ '168.y': [115.54, 89.2],
+ '169.height': [1.28, 0.99],
+ '169.width': [1.28, 0.99],
+ '169.x': [205.78, 201.04],
+ '169.y': [119.4, 92.18],
+ '170.height': [2.57, 1.98],
+ '170.width': [2.76, 2.13],
+ '170.x': [196.27, 193.7],
+ '170.y': [82.2, 63.46],
+ '171.height': [1.41, 1.09],
+ '171.width': [1.78, 1.37],
+ '171.x': [199.65, 196.31],
+ '171.y': [85.09, 65.69],
+ '172.height': [3.92, 3.03],
+ '172.width': [2.97, 2.3],
+ '172.x': [198.48, 195.41],
+ '172.y': [81.24, 62.72],
+ '173.height': [1.6, 1.24],
+ '173.width': [1.35, 1.04],
+ '173.x': [198.21, 195.2],
+ '173.y': [83.91, 64.78],
+ '174.height': [1.39, 1.07],
+ '174.width': [1.22, 0.94],
+ '174.x': [199.37, 196.09],
+ '174.y': [84.16, 64.97],
+ '175.height': [0.65, 0.5],
+ '175.width': [0.76, 0.58],
+ '175.x': [140.65, 150.76],
+ '175.y': [110.74, 85.5],
+ '176.height': [6.32, 4.88],
+ '176.width': [8.17, 6.31],
+ '176.x': [202.1, 198.2],
+ '176.y': [109.75, 84.73],
+ },
+ legend: {
+ 'continuousLegend.bounds.height': [61, 99],
+ 'continuousLegend.bar.y': [111.3, 126.3],
+ },
+ },
+ style: {
+ 'legends.continuousLegend.title': ['gdp ($)', 'gdp'],
+ },
+ },
+ '../gallery/maps.stories.tsx.worldMercatorSpec @1280': {
+ reason:
+ 'Approved B3 static map captions and neutral-class context reserve measured key space; occupied-bounds size-key placement has no legacy layout opt-out.',
+ changes: {
+ dims: {
+ 'mapSize.height': [162, 124],
+ },
+ area: {
+ 'rect.height': [162, 124],
+ 'rect.y': [153.2, 191.2],
+ },
+ marks: {
+ '0.height': [1.65, 1.26],
+ '0.width': [250.45, 191.71],
+ '0.x': [494.77, 524.15],
+ '0.y': [126.54, 96.86],
+ '1.height': [7.55, 5.78],
+ '1.width': [7.64, 5.85],
+ '1.x': [640.41, 635.62],
+ '1.y': [115.9, 88.72],
+ '2.height': [5.09, 3.89],
+ '2.width': [5.84, 4.47],
+ '2.x': [608.13, 610.91],
+ '2.y': [95.21, 72.88],
+ '3.height': [80.79, 61.84],
+ '3.width': [61.46, 47.05],
+ '3.x': [521.91, 544.92],
+ '3.y': [2.51, 1.92],
+ '4.height': [58.62, 44.87],
+ '4.width': [72.93, 55.82],
+ '4.x': [500.48, 528.52],
+ '4.y': [43.21, 33.08],
+ '5.height': [15.46, 11.84],
+ '5.width': [28.45, 21.78],
+ '5.x': [652.33, 644.74],
+ '5.y': [68.77, 52.64],
+ '6.height': [7.84, 6],
+ '6.width': [11.91, 9.12],
+ '6.x': [658.91, 649.78],
+ '6.y': [79.53, 60.88],
+ '7.height': [5.71, 4.37],
+ '7.width': [10.45, 8],
+ '7.x': [718.1, 695.09],
+ '7.y': [116.98, 89.54],
+ '8.height': [11.06, 8.47],
+ '8.width': [31.82, 24.36],
+ '8.x': [686.3, 670.75],
+ '8.y': [111.43, 85.29],
+ '9.height': [30.74, 23.53],
+ '9.width': [13.77, 10.54],
+ '9.x': [568.93, 580.91],
+ '9.y': [130.81, 100.13],
+ '10.height': [34.33, 26.28],
+ '10.width': [6.04, 4.63],
+ '10.x': [567.37, 579.72],
+ '10.y': [127.67, 97.72],
+ '11.height': [12.97, 9.93],
+ '11.width': [13.21, 10.11],
+ '11.x': [628.47, 626.49],
+ '11.y': [111.58, 85.41],
+ '12.height': [9.6, 7.35],
+ '12.width': [7.06, 5.41],
+ '12.x': [648.51, 641.82],
+ '12.y': [106.82, 81.76],
+ '13.height': [7.09, 5.43],
+ '13.width': [5.54, 4.24],
+ '13.x': [643.58, 638.05],
+ '13.y': [111.41, 85.27],
+ '14.height': [9.95, 7.62],
+ '14.width': [11.46, 8.77],
+ '14.x': [635.26, 631.68],
+ '14.y': [99.55, 76.2],
+ '15.height': [11.58, 8.86],
+ '15.width': [7.2, 5.51],
+ '15.x': [629.42, 627.21],
+ '15.y': [98.48, 75.38],
+ '16.height': [1.39, 1.06],
+ '16.width': [1.97, 1.51],
+ '16.x': [568.2, 580.35],
+ '16.y': [101.1, 77.39],
+ '17.height': [1.68, 1.29],
+ '17.width': [2.52, 1.93],
+ '17.x': [569.95, 581.69],
+ '17.y': [101.12, 77.4],
+ '18.height': [70.99, 54.34],
+ '18.width': [250.45, 191.71],
+ '18.x': [494.77, 524.15],
+ '18.y': [12.78, 9.79],
+ '19.height': [2.56, 1.96],
+ '19.width': [1.38, 1.05],
+ '19.x': [565.05, 577.94],
+ '19.y': [95.69, 73.25],
+ '20.height': [1.35, 1.03],
+ '20.width': [2.4, 1.84],
+ '20.x': [577.42, 587.41],
+ '20.y': [156.73, 119.97],
+ '21.height': [49.94, 38.23],
+ '21.width': [18.3, 14.01],
+ '21.x': [623.47, 622.66],
+ '21.y': [15.41, 11.79],
+ '22.height': [62.7, 47.99],
+ '22.width': [42.5, 32.53],
+ '22.x': [569.01, 580.97],
+ '23.height': [1.22, 0.94],
+ '23.width': [1.28, 0.98],
+ '23.x': [667.81, 656.59],
+ '23.y': [154.06, 117.92],
+ '24.height': [0.79, 0.6],
+ '24.width': [1.65, 1.26],
+ '24.x': [706.94, 686.55],
+ '24.y': [121.02, 92.63],
+ '25.height': [10.1, 7.73],
+ '25.width': [11.47, 8.78],
+ '25.x': [631.37, 628.7],
+ '25.y': [131.01, 100.28],
+ '26.height': [1.6, 1.22],
+ '26.width': [1.62, 1.24],
+ '26.x': [638.78, 634.38],
+ '26.y': [136.06, 104.14],
+ '27.height': [13.89, 10.63],
+ '27.width': [21.09, 16.14],
+ '27.x': [538.51, 557.63],
+ '27.y': [91.13, 69.75],
+ '28.height': [4, 3.06],
+ '28.width': [3.63, 2.78],
+ '28.x': [579.35, 588.89],
+ '28.y': [137.23, 105.04],
+ '29.height': [28.64, 21.92],
+ '29.width': [27.31, 20.9],
+ '29.x': [568.53, 580.6],
+ '29.y': [111.59, 85.41],
+ '30.height': [9.53, 7.29],
+ '30.width': [8.41, 6.44],
+ '30.x': [571.59, 582.94],
+ '30.y': [122.07, 93.43],
+ '31.height': [12.95, 9.91],
+ '31.width': [8.87, 6.79],
+ '31.x': [563.36, 576.65],
+ '31.y': [115.28, 88.24],
+ '32.height': [11.71, 8.97],
+ '32.width': [8.43, 6.45],
+ '32.x': [565.05, 577.94],
+ '32.y': [106.52, 81.54],
+ '33.height': [1.68, 1.29],
+ '33.width': [3.98, 3.05],
+ '33.x': [562.28, 575.82],
+ '33.y': [108.53, 83.07],
+ '34.height': [2.11, 1.62],
+ '34.width': [2.36, 1.81],
+ '34.x': [560.21, 574.23],
+ '34.y': [107.39, 82.2],
+ '35.height': [3.06, 2.34],
+ '35.width': [3.15, 2.41],
+ '35.x': [559.01, 573.31],
+ '35.y': [104.67, 80.12],
+ '36.height': [2.17, 1.66],
+ '36.width': [4.32, 3.31],
+ '36.x': [557.84, 572.42],
+ '36.y': [103.96, 79.57],
+ '37.height': [0.91, 0.7],
+ '37.width': [1.65, 1.26],
+ '37.x': [557.32, 572.02],
+ '37.y': [105.1, 80.45],
+ '38.height': [2.95, 2.26],
+ '38.width': [2.79, 2.13],
+ '38.x': [555.84, 570.89],
+ '38.y': [102.64, 78.56],
+ '39.height': [1.9, 1.46],
+ '39.width': [0.78, 0.6],
+ '39.x': [557.92, 572.48],
+ '39.y': [102.14, 78.18],
+ '40.height': [8.02, 6.14],
+ '40.width': [9.42, 7.21],
+ '40.x': [569, 580.96],
+ '40.y': [106.72, 81.69],
+ '41.height': [4.96, 3.8],
+ '41.width': [3.39, 2.59],
+ '41.x': [577.28, 587.3],
+ '41.y': [109.4, 83.74],
+ '42.height': [2.93, 2.25],
+ '42.width': [2.84, 2.18],
+ '42.x': [579.62, 589.09],
+ '42.y': [111.04, 85],
+ '43.height': [40.12, 30.71],
+ '43.width': [44.58, 34.13],
+ '43.x': [582.07, 590.96],
+ '43.y': [73.7, 56.41],
+ '44.height': [4.42, 3.38],
+ '44.width': [3.99, 3.05],
+ '44.x': [563.67, 576.88],
+ '44.y': [114.28, 87.48],
+ '45.height': [0.42, 0.32],
+ '45.width': [1.15, 0.88],
+ '45.x': [573.22, 584.19],
+ '45.y': [102.13, 78.17],
+ '46.height': [0.6, 0.46],
+ '46.width': [1.49, 1.14],
+ '46.x': [565.5, 578.28],
+ '46.y': [102.13, 78.17],
+ '47.height': [2.49, 1.91],
+ '47.width': [7.51, 5.75],
+ '47.x': [560.88, 574.75],
+ '47.y': [98.65, 75.51],
+ '48.height': [4.98, 3.81],
+ '48.width': [5.28, 4.04],
+ '48.x': [637.58, 633.45],
+ '48.y': [126.17, 96.57],
+ '49.height': [6.9, 5.28],
+ '49.width': [6.63, 5.08],
+ '49.x': [633.84, 630.59],
+ '49.y': [127.73, 97.77],
+ '50.height': [9.17, 7.02],
+ '50.width': [9.29, 7.11],
+ '50.x': [628.16, 626.25],
+ '50.y': [127.21, 97.37],
+ '51.height': [3.07, 2.35],
+ '51.width': [4.28, 3.28],
+ '51.x': [607.74, 610.61],
+ '51.y': [103.53, 79.25],
+ '52.height': [10.89, 8.34],
+ '52.width': [11.44, 8.76],
+ '52.x': [611.53, 613.52],
+ '52.y': [97.29, 74.47],
+ '53.height': [9.55, 7.31],
+ '53.width': [8.45, 6.46],
+ '53.x': [608.13, 610.91],
+ '53.y': [95.41, 73.03],
+ '54.height': [4.3, 3.29],
+ '54.width': [2.1, 1.61],
+ '54.x': [620.54, 620.41],
+ '54.y': [106.67, 81.64],
+ '55.height': [8.64, 6.61],
+ '55.width': [10.86, 8.31],
+ '55.x': [620.21, 620.16],
+ '55.y': [98.44, 75.35],
+ '56.height': [6.79, 5.2],
+ '56.width': [8.27, 6.33],
+ '56.x': [621.87, 621.43],
+ '56.y': [105.5, 80.75],
+ '57.height': [7.82, 5.99],
+ '57.width': [5.23, 4.01],
+ '57.x': [625.9, 624.52],
+ '57.y': [106.22, 81.31],
+ '58.height': [3.58, 2.74],
+ '58.width': [1.33, 1.02],
+ '58.y': [107.53, 82.31],
+ '59.height': [4.49, 3.44],
+ '59.width': [3, 2.29],
+ '59.x': [617.74, 618.27],
+ '59.y': [107.47, 82.26],
+ '60.height': [4.34, 3.32],
+ '60.width': [4.2, 3.22],
+ '60.x': [614.02, 615.42],
+ '60.y': [107.88, 82.57],
+ '61.height': [3.73, 2.85],
+ '61.width': [5.08, 3.89],
+ '61.x': [609.47, 611.94],
+ '61.y': [106.42, 81.45],
+ '62.height': [1.13, 0.86],
+ '62.width': [2.07, 1.59],
+ '62.x': [608.4, 611.12],
+ '62.y': [106.39, 81.43],
+ '63.height': [2.93, 2.24],
+ '63.width': [2.71, 2.08],
+ '63.x': [612.04, 613.91],
+ '63.y': [109.28, 83.65],
+ '64.height': [2.29, 1.76],
+ '64.width': [2.1, 1.61],
+ '64.x': [610.78, 612.95],
+ '64.y': [108.22, 82.83],
+ '65.height': [3.92, 3],
+ '65.width': [5.32, 4.07],
+ '65.x': [616.19, 617.09],
+ '65.y': [104.6, 80.07],
+ '66.height': [6.22, 4.76],
+ '66.width': [8.98, 6.88],
+ '66.x': [630.06, 627.7],
+ '66.y': [107.44, 82.24],
+ '67.height': [6.1, 4.67],
+ '67.width': [5.12, 3.92],
+ '67.x': [627.72, 625.91],
+ '67.y': [112.65, 86.22],
+ '68.height': [4.39, 3.36],
+ '68.width': [3.92, 3],
+ '68.x': [626.12, 624.68],
+ '68.y': [113.62, 86.97],
+ '69.height': [0.89, 0.68],
+ '69.width': [1.38, 1.05],
+ '69.x': [626.47, 624.95],
+ '69.y': [113.65, 86.99],
+ '70.height': [6.95, 5.32],
+ '70.width': [8.07, 6.18],
+ '70.x': [635.23, 631.65],
+ '70.y': [120.99, 92.61],
+ '71.height': [5.41, 4.14],
+ '71.width': [2.15, 1.64],
+ '71.x': [642.74, 637.41],
+ '71.y': [121.69, 93.15],
+ '72.height': [12.1, 9.26],
+ '72.width': [7.37, 5.64],
+ '72.x': [641, 636.07],
+ '72.y': [122.46, 93.73],
+ '73.height': [1.26, 0.97],
+ '73.width': [0.97, 0.74],
+ '73.x': [641.34, 636.34],
+ '73.y': [133.72, 102.36],
+ '74.height': [9.59, 7.34],
+ '74.width': [8.65, 6.62],
+ '74.x': [628.1, 626.2],
+ '74.y': [118.33, 90.58],
+ '75.height': [1.5, 1.15],
+ '75.width': [1.2, 0.92],
+ '75.x': [640.19, 635.46],
+ '75.y': [116.88, 89.46],
+ '76.height': [3.08, 2.36],
+ '76.width': [1.09, 0.84],
+ '76.x': [643.84, 638.25],
+ '76.y': [90.67, 69.4],
+ '77.height': [1.3, 1],
+ '77.width': [1.03, 0.79],
+ '77.x': [644.44, 638.71],
+ '77.y': [89.52, 68.52],
+ '78.height': [10, 7.65],
+ '78.width': [5.03, 3.85],
+ '78.x': [650.09, 643.03],
+ '78.y': [123.68, 94.67],
+ '79.height': [0.97, 0.74],
+ '79.width': [0.43, 0.33],
+ '79.x': [644.3, 638.6],
+ '79.y': [91.29, 69.87],
+ '80.height': [0.53, 0.41],
+ '80.width': [2.09, 1.6],
+ '80.x': [608.28, 611.03],
+ '80.y': [105.49, 80.75],
+ '81.height': [5.91, 4.52],
+ '81.width': [2.76, 2.11],
+ '81.x': [625.24, 624.01],
+ '81.y': [87.19, 66.74],
+ '82.height': [14.34, 10.97],
+ '82.width': [14.39, 11.02],
+ '82.x': [613.96, 615.38],
+ '82.y': [87.4, 66.9],
+ '83.height': [3.41, 2.61],
+ '83.width': [2.97, 2.28],
+ '83.x': [644.3, 638.6],
+ '83.y': [90.58, 69.34],
+ '84.height': [2.72, 2.08],
+ '84.width': [3.35, 2.57],
+ '84.x': [655.88, 647.47],
+ '84.y': [96.46, 73.83],
+ '85.height': [1.2, 0.92],
+ '85.width': [0.6, 0.46],
+ '85.x': [655.3, 647.02],
+ '85.y': [96.41, 73.8],
+ '86.height': [1.22, 0.94],
+ '86.width': [1.28, 0.98],
+ '86.x': [652.4, 644.8],
+ '86.y': [93.3, 71.41],
+ '87.height': [6.9, 5.28],
+ '87.width': [6.8, 5.2],
+ '87.x': [646.99, 640.66],
+ '87.y': [87.16, 66.72],
+ '88.height': [7.3, 5.59],
+ '88.width': [5.43, 4.16],
+ '88.x': [656.18, 647.69],
+ '88.y': [96.19, 73.63],
+ '89.height': [1.42, 1.09],
+ '89.width': [0.85, 0.65],
+ '89.x': [735.93, 708.73],
+ '89.y': [125.53, 96.09],
+ '90.height': [2.91, 2.23],
+ '90.width': [3.66, 2.8],
+ '90.x': [691.2, 674.5],
+ '90.y': [105, 80.37],
+ '91.height': [10.55, 8.08],
+ '91.width': [5.71, 4.37],
+ '91.x': [687.74, 671.85],
+ '91.y': [100.73, 77.1],
+ '92.height': [6.29, 4.82],
+ '92.width': [5.18, 3.97],
+ '92.x': [689.65, 673.31],
+ '92.y': [99.2, 75.93],
+ '93.height': [13.62, 10.43],
+ '93.width': [6.18, 4.73],
+ '93.x': [684.22, 669.15],
+ '93.y': [94.67, 72.47],
+ '94.height': [10.71, 8.2],
+ '94.width': [4.98, 3.81],
+ '94.x': [691.08, 674.41],
+ '94.y': [98.53, 75.42],
+ '95.height': [4.86, 3.72],
+ '95.width': [4.53, 3.47],
+ '95.x': [706.45, 686.17],
+ '95.y': [82.06, 62.81],
+ '96.height': [3.66, 2.8],
+ '96.width': [2.33, 1.78],
+ '96.x': [707.74, 687.16],
+ '96.y': [86.08, 65.89],
+ '97.height': [10.67, 8.17],
+ '97.width': [22.28, 17.05],
+ '97.x': [681.05, 666.73],
+ '97.y': [72.69, 55.64],
+ '98.height': [20.88, 15.98],
+ '98.width': [20.33, 15.56],
+ '98.x': [667.43, 656.3],
+ '98.y': [88.8, 67.97],
+ '99.height': [4.39, 3.36],
+ '99.width': [3.19, 2.44],
+ '99.x': [681.28, 666.91],
+ '99.y': [96.15, 73.6],
+ '100.height': [1.24, 0.95],
+ '100.width': [2.29, 1.75],
+ '100.x': [681.79, 667.3],
+ '100.y': [94.7, 72.49],
+ '101.height': [3.18, 2.44],
+ '101.width': [5.63, 4.31],
+ '101.x': [675.72, 662.65],
+ '101.y': [93.01, 71.19],
+ '102.height': [10.89, 8.33],
+ '102.width': [11.8, 9.03],
+ '102.x': [662.35, 652.42],
+ '102.y': [87.38, 66.89],
+ '103.height': [7.7, 5.89],
+ '103.width': [10.18, 7.79],
+ '103.x': [662.11, 652.23],
+ '103.y': [86.19, 65.97],
+ '104.height': [3.77, 2.89],
+ '104.width': [5.24, 4.01],
+ '104.x': [666.92, 655.91],
+ '104.y': [83.95, 64.26],
+ '105.height': [3.72, 2.85],
+ '105.width': [7.51, 5.75],
+ '105.x': [668.33, 656.99],
+ '105.y': [81.76, 62.58],
+ '106.height': [6.71, 5.14],
+ '106.width': [9.77, 7.48],
+ '106.x': [656.53, 647.96],
+ '106.y': [82.28, 62.98],
+ '107.height': [12.12, 9.28],
+ '107.width': [13.36, 10.23],
+ '107.x': [650.69, 643.49],
+ '107.y': [85.09, 65.13],
+ '108.height': [4.17, 3.19],
+ '108.width': [4.63, 3.54],
+ '108.x': [644.84, 639.01],
+ '108.y': [87.3, 66.82],
+ '109.height': [2.28, 1.74],
+ '109.width': [2.03, 1.56],
+ '109.x': [650.32, 643.21],
+ '109.y': [83.69, 64.06],
+ '110.height': [20.94, 16.03],
+ '110.width': [8.96, 6.86],
+ '110.x': [627.67, 625.87],
+ '110.y': [47.85, 36.62],
+ '111.height': [5.71, 4.37],
+ '111.width': [6.6, 5.06],
+ '111.x': [636.14, 632.35],
+ '111.y': [67.79, 51.89],
+ '112.height': [7.46, 5.71],
+ '112.width': [12.52, 9.58],
+ '112.x': [635.36, 631.76],
+ '112.y': [72.37, 55.39],
+ '113.height': [6.58, 5.04],
+ '113.width': [6.93, 5.3],
+ '113.x': [629.79, 627.49],
+ '113.y': [69.41, 53.13],
+ '114.height': [2.7, 2.06],
+ '114.width': [5.22, 3.99],
+ '114.x': [626.6, 625.05],
+ '114.y': [75.99, 58.16],
+ '115.height': [2.93, 2.25],
+ '115.width': [4.53, 3.47],
+ '115.x': [631.27, 628.63],
+ '115.y': [76.42, 58.5],
+ '116.height': [3.04, 2.33],
+ '116.width': [2.37, 1.81],
+ '116.x': [638.52, 634.18],
+ '116.y': [76.59, 58.62],
+ '117.height': [4.54, 3.47],
+ '117.width': [6.54, 5.01],
+ '117.x': [634.07, 630.77],
+ '117.y': [76.85, 58.82],
+ '118.height': [3, 2.3],
+ '118.width': [3.85, 2.95],
+ '118.x': [634.65, 631.21],
+ '118.y': [67.54, 51.7],
+ '119.height': [2.99, 2.29],
+ '119.width': [4.95, 3.79],
+ '119.x': [634.65, 631.21],
+ '119.y': [65.49, 50.13],
+ '120.height': [2.85, 2.18],
+ '120.width': [3.33, 2.55],
+ '120.x': [636.24, 632.43],
+ '120.y': [63.29, 48.44],
+ '121.height': [8.54, 6.54],
+ '121.width': [6.28, 4.81],
+ '121.x': [624.17, 623.19],
+ '121.y': [69.25, 53.01],
+ '122.height': [2.84, 2.18],
+ '122.width': [4.3, 3.29],
+ '122.x': [635.57, 631.92],
+ '122.y': [80.86, 61.89],
+ '123.height': [6.14, 4.7],
+ '123.width': [4.49, 3.44],
+ '123.x': [634.02, 630.73],
+ '123.y': [83.15, 63.65],
+ '124.height': [5.66, 4.33],
+ '124.width': [13.04, 9.98],
+ '124.x': [638.12, 633.87],
+ '124.y': [82.86, 63.42],
+ '125.height': [2.83, 2.17],
+ '125.width': [1.19, 0.91],
+ '125.x': [633.43, 630.28],
+ '125.y': [82.34, 63.03],
+ '126.height': [3.93, 3.01],
+ '126.width': [3.99, 3.05],
+ '126.x': [629.5, 627.27],
+ '126.y': [78.61, 60.17],
+ '127.height': [2.09, 1.6],
+ '127.width': [3.08, 2.35],
+ '127.x': [624.19, 623.21],
+ '127.y': [77.25, 59.13],
+ '128.height': [0.74, 0.57],
+ '128.width': [0.4, 0.3],
+ '128.x': [623.95, 623.02],
+ '128.y': [74.82, 57.27],
+ '129.height': [2.13, 1.63],
+ '129.width': [2.53, 1.94],
+ '129.x': [621.75, 621.34],
+ '129.y': [73.34, 56.13],
+ '130.height': [3.07, 2.35],
+ '130.width': [2.63, 2.01],
+ '130.x': [622.31, 621.76],
+ '130.y': [71.01, 54.35],
+ '131.height': [4.91, 3.76],
+ '131.width': [2.18, 1.67],
+ '131.x': [613.37, 614.93],
+ '131.y': [82.73, 63.32],
+ '132.height': [7.08, 5.42],
+ '132.width': [8.65, 6.62],
+ '132.x': [613.46, 615],
+ '132.y': [81.33, 62.25],
+ '133.height': [4.04, 3.1],
+ '133.width': [2.75, 2.1],
+ '133.x': [613.06, 614.69],
+ '133.y': [69.07, 52.87],
+ '134.height': [1.71, 1.31],
+ '134.width': [2.15, 1.64],
+ '134.x': [734.12, 707.35],
+ '134.y': [129.53, 99.14],
+ '135.height': [2.98, 2.28],
+ '135.width': [4.11, 3.15],
+ '135.x': [728.87, 703.33],
+ '135.y': [119.84, 91.73],
+ '136.height': [11.21, 8.58],
+ '136.width': [8.35, 6.39],
+ '136.x': [735.84, 708.67],
+ '136.y': [140.8, 107.77],
+ '137.height': [26.34, 20.16],
+ '137.width': [27.99, 21.42],
+ '137.x': [698.85, 680.35],
+ '137.y': [122.71, 93.92],
+ '138.height': [2.71, 2.07],
+ '138.width': [1.46, 1.11],
+ '138.x': [675.44, 662.44],
+ '138.y': [108.37, 82.95],
+ '139.height': [31.3, 23.95],
+ '139.width': [42.68, 32.67],
+ '139.x': [671.26, 659.23],
+ '139.y': [71.07, 54.4],
+ '140.height': [2.53, 1.93],
+ '140.width': [1.28, 0.98],
+ '140.x': [703.56, 683.96],
+ '140.y': [97.04, 74.28],
+ '141.height': [9.84, 7.53],
+ '141.width': [8.16, 6.25],
+ '141.x': [624.69, 623.59],
+ '141.y': [77.99, 59.7],
+ '142.height': [3.67, 2.81],
+ '142.width': [3.2, 2.45],
+ '142.x': [625.63, 624.31],
+ '142.y': [65.8, 50.37],
+ '143.height': [10.39, 7.95],
+ '143.width': [6.44, 4.93],
+ '143.x': [614.73, 615.97],
+ '143.y': [64.61, 49.45],
+ '144.height': [5, 3.82],
+ '144.width': [7.46, 5.71],
+ '144.x': [603.08, 607.05],
+ '144.y': [52.61, 40.27],
+ '145.height': [3.26, 2.5],
+ '145.width': [3.89, 2.98],
+ '145.x': [651.16, 643.85],
+ '145.y': [83.12, 63.62],
+ '146.height': [2.34, 1.79],
+ '146.width': [4.65, 3.56],
+ '146.x': [647.8, 641.28],
+ '146.y': [81.52, 62.4],
+ '147.height': [9.22, 7.05],
+ '147.width': [6.51, 4.98],
+ '147.x': [701.52, 682.4],
+ '147.y': [102.14, 78.18],
+ '148.height': [4.29, 3.29],
+ '148.width': [13.28, 10.17],
+ '148.x': [689.63, 673.3],
+ '148.y': [110.41, 84.51],
+ '149.height': [1.01, 0.77],
+ '149.width': [0.87, 0.66],
+ '149.x': [699.45, 680.82],
+ '149.y': [111.45, 85.31],
+ '150.height': [1.41, 1.08],
+ '150.width': [2, 1.53],
+ '150.x': [629.53, 627.29],
+ '150.y': [78.26, 59.9],
+ '151.height': [17.23, 13.19],
+ '151.width': [7.56, 5.79],
+ '151.x': [634.36, 630.99],
+ '151.y': [45.73, 35.01],
+ '152.height': [1.91, 1.46],
+ '152.width': [3.95, 3.03],
+ '152.x': [631.74, 628.99],
+ '152.y': [75.42, 57.73],
+ '153.height': [2.76, 2.12],
+ '153.width': [4.6, 3.52],
+ '153.x': [628.51, 626.52],
+ '153.y': [73.73, 56.44],
+ '154.height': [4, 3.06],
+ '154.width': [4.7, 3.6],
+ '154.x': [645.27, 639.34],
+ '154.y': [102.51, 78.47],
+ '155.height': [12.95, 9.91],
+ '155.width': [11.23, 8.59],
+ '155.x': [710.03, 688.91],
+ '155.y': [79.57, 60.9],
+ '156.height': [6.23, 4.77],
+ '156.width': [5.84, 4.47],
+ '156.x': [576.39, 586.62],
+ '156.y': [128.96, 98.71],
+ '157.height': [4.64, 3.55],
+ '157.width': [7.31, 5.59],
+ '157.x': [649.64, 642.69],
+ '157.y': [101.78, 77.9],
+ '158.height': [12.12, 9.28],
+ '158.width': [14.63, 11.2],
+ '158.x': [644.09, 638.44],
+ '158.y': [91.59, 70.11],
+ '159.height': [67.94, 52],
+ '159.width': [250.45, 191.71],
+ '159.x': [494.77, 524.15],
+ '159.y': [172.53, 132.06],
+ '160.height': [0.57, 0.44],
+ '160.width': [1.28, 0.98],
+ '160.x': [642.77, 637.43],
+ '160.y': [88.65, 67.85],
+ '161.height': [0.51, 0.39],
+ '161.width': [1.21, 0.93],
+ '161.x': [642.44, 637.18],
+ '161.y': [89.07, 68.18],
+ '162.height': [11.41, 8.73],
+ '162.width': [11.06, 8.46],
+ '162.x': [608.16, 610.94],
+ '162.y': [88.57, 67.8],
+ '163.height': [7.48, 5.73],
+ '163.width': [8.46, 6.48],
+ '163.x': [637.19, 633.15],
+ '163.y': [92.06, 70.47],
+ '164.height': [10.56, 8.09],
+ '164.width': [11.03, 8.44],
+ '164.x': [626.48, 624.96],
+ '164.y': [90.78, 69.49],
+ '165.height': [8.15, 6.24],
+ '165.width': [10.32, 7.9],
+ '165.x': [642.93, 637.55],
+ '165.y': [104.72, 80.15],
+ '166.height': [1.26, 0.97],
+ '166.width': [1.15, 0.88],
+ '166.x': [648.98, 642.19],
+ '166.y': [106.33, 81.39],
+ '167.height': [2.45, 1.87],
+ '167.width': [4.45, 3.4],
+ '167.x': [649.61, 642.66],
+ '167.y': [107.21, 82.07],
+ '168.height': [3.96, 3.03],
+ '168.width': [3.8, 2.91],
+ '168.x': [640.58, 635.75],
+ '168.y': [112.28, 85.95],
+ '169.height': [1.24, 0.95],
+ '169.width': [1.24, 0.95],
+ '169.x': [640.19, 635.46],
+ '169.y': [116.03, 88.82],
+ '170.height': [2.5, 1.91],
+ '170.width': [2.68, 2.05],
+ '170.x': [630.96, 628.39],
+ '170.y': [79.88, 61.14],
+ '171.height': [1.37, 1.05],
+ '171.width': [1.73, 1.32],
+ '171.x': [634.24, 630.9],
+ '171.y': [82.69, 63.29],
+ '172.height': [3.81, 2.92],
+ '172.width': [2.89, 2.21],
+ '172.x': [633.1, 630.03],
+ '172.y': [78.95, 60.43],
+ '173.height': [1.56, 1.19],
+ '173.width': [1.31, 1.01],
+ '173.x': [632.83, 629.82],
+ '173.y': [81.55, 62.42],
+ '174.height': [1.35, 1.03],
+ '174.width': [1.18, 0.91],
+ '174.x': [633.96, 630.69],
+ '174.y': [81.79, 62.6],
+ '175.height': [0.63, 0.48],
+ '175.width': [0.73, 0.56],
+ '175.x': [576.9, 587.01],
+ '175.y': [107.62, 82.38],
+ '176.height': [6.14, 4.7],
+ '176.width': [7.94, 6.08],
+ '176.x': [636.62, 632.72],
+ '176.y': [106.66, 81.64],
+ },
+ legend: {
+ 'continuousLegend.bounds.height': [61, 99],
+ 'continuousLegend.bar.y': [107.2, 122.2],
+ },
+ },
+ style: {
+ 'legends.continuousLegend.title': ['gdp ($)', 'gdp'],
+ },
+ },
+ '../gallery/maps.stories.tsx.pointLayerSpec @390': {
+ reason:
+ 'Approved B3 static map captions and neutral-class context reserve measured key space; occupied-bounds size-key placement has no legacy layout opt-out.',
+ changes: {
+ dims: {
+ 'mapSize.height': [257.55, 242.55],
+ },
+ area: {
+ 'rect.height': [257.55, 242.55],
+ 'rect.y': [98.85, 113.85],
+ },
+ marks: {
+ '0.y': [151.51, 144.01],
+ '1.y': [179.12, 171.62],
+ '2.y': [129.92, 122.42],
+ '3.y': [105.17, 97.67],
+ '4.y': [179.21, 171.71],
+ '5.y': [149.56, 142.06],
+ '6.y': [100.85, 93.35],
+ '7.y': [116.45, 108.95],
+ '8.y': [42.85, 35.35],
+ '9.y': [78.36, 70.86],
+ '10.y': [48.24, 40.74],
+ '11.y': [94.38, 86.88],
+ '12.y': [130.77, 123.27],
+ '13.y': [49.39, 41.89],
+ '14.y': [138.39, 130.89],
+ '15.y': [90.52, 83.02],
+ '16.y': [71.71, 64.21],
+ '17.y': [142.09, 134.59],
+ '18.y': [74.23, 66.73],
+ '19.y': [85.82, 78.32],
+ '20.y': [112.45, 104.95],
+ '21.y': [105.81, 98.31],
+ '22.y': [97.11, 89.61],
+ '23.y': [134.69, 127.19],
+ '24.y': [142.58, 135.08],
+ '25.y': [80.05, 72.55],
+ '26.y': [106.42, 98.92],
+ '27.y': [114.34, 106.84],
+ '28.y': [199.8, 192.3],
+ '29.y': [90.89, 83.39],
+ '30.y': [120.13, 112.63],
+ '31.y': [107.58, 100.08],
+ '32.y': [55.47, 47.97],
+ '33.y': [152.91, 145.41],
+ '34.y': [40.73, 33.23],
+ '35.y': [61.33, 53.83],
+ '36.y': [66.35, 58.85],
+ '37.y': [95.98, 88.48],
+ '38.y': [50.36, 42.86],
+ '39.y': [136.44, 128.94],
+ '40.y': [93.42, 85.92],
+ '41.y': [112.3, 104.8],
+ '42.y': [32.25, 24.75],
+ '43.y': [64.3, 56.8],
+ '44.y': [93.41, 85.91],
+ '45.y': [146.66, 139.16],
+ '46.y': [39.71, 32.21],
+ '47.y': [86.3, 78.8],
+ '48.y': [63.97, 56.47],
+ '49.y': [168.88, 161.38],
+ '50.y': [85.27, 77.77],
+ '51.cy': [193.65, 186.15],
+ '52.cy': [195.96, 188.46],
+ '53.cy': [171.28, 163.78],
+ '54.cy': [189.79, 182.29],
+ '55.cy': [175.28, 167.78],
+ },
+ legend: {
+ 'pointCategoricalLegend.bounds.height': [16, 31],
+ 'pointSizeLegend.bounds.height': [71.48, 72],
+ 'pointSizeLegend.bounds.width': [66.79, 69],
+ 'pointSizeLegend.bounds.x': [295.21, 293],
+ 'pointSizeLegend.bounds.y': [266.92, 266.4],
+ 'pointCategoricalLegend.entry.0.y': [undefined, 89.85],
+ 'pointCategoricalLegend.entry.1.y': [undefined, 89.85],
+ 'pointCategoricalLegend.entry.2.y': [undefined, 89.85],
+ },
+ },
+ style: {},
+ },
+ '../gallery/maps.stories.tsx.pointLayerSpec @1280': {
+ reason:
+ 'Approved B3 static map captions and neutral-class context reserve measured key space; occupied-bounds size-key placement has no legacy layout opt-out.',
+ changes: {
+ dims: {
+ 'mapSize.height': [215.2, 200.2],
+ },
+ area: {
+ 'rect.height': [215.2, 200.2],
+ 'rect.y': [121.2, 136.2],
+ },
+ marks: {
+ '0.height': [33.34, 30.49],
+ '0.width': [20.42, 18.68],
+ '0.x': [676.62, 671.77],
+ '0.y': [128.23, 118.97],
+ '1.height': [41.2, 37.67],
+ '1.width': [76.9, 70.32],
+ '1.x': [470.26, 483.08],
+ '1.y': [153.29, 141.88],
+ '2.height': [42.19, 38.58],
+ '2.width': [36.1, 33.01],
+ '2.x': [529.28, 537.04],
+ '2.y': [108.64, 101.05],
+ '3.height': [30.56, 27.94],
+ '3.width': [38.56, 35.26],
+ '3.x': [565.38, 570.05],
+ '3.y': [86.17, 80.51],
+ '4.height': [41.83, 38.25],
+ '4.width': [48.35, 44.21],
+ '4.x': [682.15, 676.83],
+ '4.y': [153.37, 141.95],
+ '5.height': [30.21, 27.63],
+ '5.width': [28.84, 26.38],
+ '5.x': [690.69, 684.63],
+ '5.y': [126.46, 117.35],
+ '6.height': [28.43, 26],
+ '6.width': [16.48, 15.07],
+ '6.x': [675.56, 670.8],
+ '6.y': [82.26, 76.93],
+ '7.height': [21.29, 19.47],
+ '7.width': [39.3, 35.93],
+ '7.x': [602.27, 603.79],
+ '7.y': [96.41, 89.87],
+ '8.height': [30.87, 28.23],
+ '8.width': [19.54, 17.86],
+ '8.x': [750.21, 739.06],
+ '8.y': [29.62, 28.79],
+ '9.height': [10.17, 9.3],
+ '9.width': [18.44, 16.86],
+ '9.x': [744.23, 733.6],
+ '9.y': [61.85, 58.27],
+ '10.height': [39.29, 35.92],
+ '10.width': [34.9, 31.91],
+ '10.x': [628.74, 627.99],
+ '10.y': [34.51, 33.27],
+ '11.height': [16.27, 14.88],
+ '11.width': [7.09, 6.49],
+ '11.x': [737.9, 727.8],
+ '11.y': [76.39, 71.56],
+ '12.height': [20.72, 18.95],
+ '12.width': [46.82, 42.81],
+ '12.x': [697.6, 690.96],
+ '12.y': [109.41, 101.76],
+ '13.height': [21.9, 20.02],
+ '13.width': [34.87, 31.88],
+ '13.x': [596.79, 598.78],
+ '13.y': [35.56, 34.23],
+ '14.height': [24.16, 22.09],
+ '14.width': [45.79, 41.87],
+ '14.x': [596.94, 598.92],
+ '14.y': [116.33, 108.08],
+ '15.height': [20, 18.29],
+ '15.width': [30.87, 28.22],
+ '15.x': [710.43, 702.69],
+ '15.y': [72.88, 68.35],
+ '16.height': [24.82, 22.7],
+ '16.width': [37.05, 33.88],
+ '16.x': [595.09, 597.22],
+ '16.y': [55.81, 52.75],
+ '17.height': [72.88, 66.64],
+ '17.width': [74.33, 67.96],
+ '17.x': [574.13, 578.06],
+ '17.y': [119.68, 111.15],
+ '18.height': [30.85, 28.21],
+ '18.width': [37.04, 33.87],
+ '18.x': [559.15, 564.36],
+ '18.y': [58.1, 54.83],
+ '19.height': [9.04, 8.26],
+ '19.width': [9.16, 8.37],
+ '19.x': [744.39, 733.74],
+ '19.y': [68.62, 64.45],
+ '20.height': [30.81, 28.17],
+ '20.width': [35.37, 32.34],
+ '20.x': [635.43, 634.11],
+ '20.y': [92.78, 86.55],
+ '21.height': [23.88, 21.84],
+ '21.width': [23.81, 21.77],
+ '21.x': [703.67, 696.51],
+ '21.y': [86.76, 81.05],
+ '22.height': [37.74, 34.51],
+ '22.width': [21.2, 19.39],
+ '22.x': [656.99, 653.83],
+ '22.y': [78.86, 73.82],
+ '23.height': [38.49, 35.2],
+ '23.width': [37.08, 33.91],
+ '23.x': [560.13, 565.25],
+ '23.y': [112.97, 105.01],
+ '24.height': [24.33, 22.25],
+ '24.width': [26.72, 24.43],
+ '24.x': [641.62, 639.77],
+ '24.y': [120.13, 111.56],
+ '25.height': [72.94, 66.7],
+ '25.width': [42.59, 38.94],
+ '25.x': [492.72, 503.62],
+ '25.y': [63.38, 59.66],
+ '26.height': [9.29, 8.49],
+ '26.width': [5.56, 5.09],
+ '26.x': [736.62, 726.63],
+ '26.y': [87.32, 81.55],
+ '27.height': [1.34, 1.23],
+ '27.width': [1.11, 1.01],
+ '27.x': [731.03, 721.53],
+ '27.y': [94.5, 88.12],
+ '28.height': [22.13, 20.24],
+ '28.width': [34.24, 31.31],
+ '28.x': [551.08, 556.98],
+ '28.y': [172.05, 159.04],
+ '29.height': [21.08, 19.27],
+ '29.width': [32.03, 29.29],
+ '29.x': [631.16, 630.21],
+ '29.y': [73.22, 68.66],
+ '30.height': [20.16, 18.44],
+ '30.width': [39.3, 35.94],
+ '30.x': [668.48, 664.33],
+ '30.y': [99.76, 92.93],
+ '31.height': [12.04, 11.01],
+ '31.width': [24.06, 22],
+ '31.x': [718.16, 709.75],
+ '31.y': [88.37, 82.51],
+ '32.height': [42.25, 38.64],
+ '32.width': [40.19, 36.75],
+ '32.x': [660.03, 656.61],
+ '32.y': [41.08, 39.27],
+ '33.height': [32.9, 30.09],
+ '33.width': [19.01, 17.38],
+ '33.x': [659.11, 655.76],
+ '33.y': [129.51, 120.13],
+ '34.height': [34.43, 31.48],
+ '34.width': [54.18, 49.54],
+ '34.x': [544.32, 550.8],
+ '34.y': [27.7, 27.04],
+ '35.height': [18.43, 16.86],
+ '35.width': [8.74, 7.99],
+ '35.x': [747.17, 736.28],
+ '35.y': [46.39, 44.13],
+ '36.height': [30.58, 27.96],
+ '36.width': [40.07, 36.64],
+ '36.x': [713.79, 705.76],
+ '36.y': [50.94, 48.29],
+ '37.height': [25.4, 23.23],
+ '37.width': [22.22, 20.31],
+ '37.x': [689.68, 683.72],
+ '37.y': [77.84, 72.89],
+ '38.height': [36.02, 32.94],
+ '38.width': [42.64, 38.99],
+ '38.x': [496.45, 507.03],
+ '38.y': [36.44, 35.03],
+ '39.height': [15.76, 14.41],
+ '39.width': [45.19, 41.32],
+ '39.x': [665.2, 661.33],
+ '39.y': [114.56, 106.46],
+ '40.height': [37.45, 34.25],
+ '40.width': [29.85, 27.29],
+ '40.x': [539.21, 546.13],
+ '40.y': [75.52, 70.76],
+ '41.height': [23.5, 21.48],
+ '41.width': [42.13, 38.53],
+ '41.x': [699.7, 692.88],
+ '41.y': [92.65, 86.43],
+ '42.height': [26.38, 24.12],
+ '42.width': [35.6, 32.55],
+ '42.x': [505.86, 515.63],
+ '43.height': [30.66, 28.03],
+ '43.width': [28.5, 26.06],
+ '43.x': [648.83, 646.36],
+ '43.y': [49.08, 46.59],
+ '44.height': [21.91, 20.03],
+ '44.width': [43.83, 40.07],
+ '44.x': [593.95, 596.18],
+ '44.y': [75.5, 70.75],
+ '45.height': [20.61, 18.85],
+ '45.width': [27.01, 24.7],
+ '45.x': [703.1, 695.99],
+ '45.y': [123.83, 114.94],
+ '46.height': [51.25, 46.86],
+ '46.width': [31.64, 28.93],
+ '46.x': [531.2, 538.8],
+ '46.y': [26.77, 26.19],
+ '47.height': [52.25, 47.78],
+ '47.width': [33.77, 30.88],
+ '47.x': [511.86, 521.12],
+ '47.y': [69.05, 64.85],
+ '48.height': [16.86, 15.42],
+ '48.width': [8.96, 8.19],
+ '48.x': [740.38, 730.07],
+ '48.y': [48.79, 46.32],
+ '49.height': [26.71, 24.43],
+ '49.width': [30.69, 28.06],
+ '49.x': [645.18, 643.03],
+ '49.y': [143.99, 133.38],
+ '50.height': [5.9, 5.39],
+ '50.width': [4.13, 3.78],
+ '50.x': [752.54, 741.2],
+ '50.y': [68.12, 64],
+ '51.cx': [637.92, 636.38],
+ '51.cy': [166.47, 153.93],
+ '52.cx': [619.73, 619.76],
+ '52.cy': [168.57, 155.85],
+ '53.cx': [629.78, 628.94],
+ '53.cy': [146.17, 135.37],
+ '54.cx': [624.21, 623.85],
+ '54.cy': [162.97, 150.73],
+ '55.cx': [575.19, 579.03],
+ '55.cy': [149.81, 138.7],
+ },
+ legend: {
+ 'pointCategoricalLegend.bounds.height': [16, 31],
+ 'pointSizeLegend.bounds.height': [71.48, 72],
+ 'pointSizeLegend.bounds.width': [66.79, 69],
+ 'pointSizeLegend.bounds.x': [1175.21, 1173],
+ 'pointSizeLegend.bounds.y': [246.92, 246.4],
+ 'pointCategoricalLegend.entry.0.y': [undefined, 112.2],
+ 'pointCategoricalLegend.entry.1.y': [undefined, 112.2],
+ 'pointCategoricalLegend.entry.2.y': [undefined, 112.2],
+ },
+ },
+ style: {},
+ },
+ '../gallery/maps.stories.tsx.readerZoomPointsSpec @390': {
+ reason:
+ 'Approved B3 static map captions and neutral-class context reserve measured key space; occupied-bounds size-key placement has no legacy layout opt-out.',
+ changes: {
+ dims: {
+ 'mapSize.height': [231.1, 216.1],
+ },
+ area: {
+ 'rect.height': [231.1, 216.1],
+ 'rect.y': [125.3, 140.3],
+ },
+ marks: {
+ '0.height': [36.37, 33.51],
+ '0.width': [22.28, 20.53],
+ '0.x': [246.76, 241.91],
+ '0.y': [138.05, 128.79],
+ '1.height': [44.93, 41.41],
+ '1.width': [83.88, 77.29],
+ '1.x': [21.67, 34.49],
+ '1.y': [165.39, 153.97],
+ '2.height': [46.02, 42.41],
+ '2.width': [39.38, 36.29],
+ '2.x': [86.04, 93.81],
+ '2.y': [116.68, 109.1],
+ '3.height': [33.33, 30.71],
+ '3.width': [42.06, 38.76],
+ '3.x': [125.42, 130.1],
+ '3.y': [92.18, 86.51],
+ '4.height': [45.62, 42.04],
+ '4.width': [52.73, 48.6],
+ '4.x': [252.79, 247.47],
+ '4.y': [165.48, 154.06],
+ '5.height': [32.96, 30.37],
+ '5.width': [31.46, 28.99],
+ '5.x': [262.1, 256.05],
+ '5.y': [136.12, 127.01],
+ '6.height': [31.01, 28.58],
+ '6.width': [17.97, 16.56],
+ '6.x': [245.6, 240.85],
+ '6.y': [87.91, 82.58],
+ '7.height': [23.22, 21.4],
+ '7.width': [42.86, 39.5],
+ '7.x': [165.66, 167.18],
+ '7.y': [103.35, 96.81],
+ '8.height': [33.67, 31.03],
+ '8.width': [21.31, 19.64],
+ '8.x': [327.02, 315.88],
+ '8.y': [30.49, 29.67],
+ '9.height': [11.09, 10.22],
+ '9.width': [20.11, 18.53],
+ '9.x': [320.51, 309.87],
+ '9.y': [65.65, 62.06],
+ '10.height': [42.85, 39.49],
+ '10.width': [38.07, 35.08],
+ '10.x': [194.53, 193.79],
+ '10.y': [35.83, 34.59],
+ '11.height': [17.75, 16.36],
+ '11.width': [7.74, 7.13],
+ '11.x': [313.6, 303.5],
+ '11.y': [81.5, 76.67],
+ '12.height': [22.61, 20.83],
+ '12.width': [51.07, 47.06],
+ '12.x': [269.65, 263],
+ '12.y': [117.53, 109.87],
+ '13.height': [23.88, 22.01],
+ '13.width': [38.03, 35.05],
+ '13.x': [159.69, 161.67],
+ '13.y': [36.97, 35.64],
+ '14.height': [26.35, 24.28],
+ '14.width': [49.95, 46.03],
+ '14.x': [159.85, 161.83],
+ '14.y': [125.07, 116.82],
+ '15.height': [21.81, 20.1],
+ '15.width': [33.67, 31.02],
+ '15.x': [283.64, 275.9],
+ '15.y': [77.68, 73.15],
+ '16.height': [27.07, 24.95],
+ '16.width': [40.41, 37.24],
+ '16.x': [157.83, 159.96],
+ '16.y': [59.06, 56],
+ '17.height': [79.49, 73.25],
+ '17.width': [81.07, 74.71],
+ '17.x': [134.97, 138.9],
+ '17.y': [128.73, 120.19],
+ '18.height': [33.65, 31],
+ '18.width': [40.41, 37.23],
+ '18.x': [118.62, 123.83],
+ '18.y': [61.55, 58.29],
+ '19.height': [9.86, 9.08],
+ '19.width': [9.99, 9.2],
+ '19.x': [320.68, 310.03],
+ '19.y': [73.03, 68.87],
+ '20.height': [33.61, 30.97],
+ '20.width': [38.58, 35.55],
+ '20.x': [201.83, 200.51],
+ '20.y': [99.39, 93.16],
+ '21.height': [26.05, 24],
+ '21.width': [25.97, 23.93],
+ '21.x': [276.27, 269.1],
+ '21.y': [92.82, 87.1],
+ '22.height': [41.16, 37.93],
+ '22.width': [23.13, 21.31],
+ '22.x': [225.35, 222.18],
+ '22.y': [84.2, 79.16],
+ '23.height': [41.99, 38.69],
+ '23.width': [40.45, 37.28],
+ '23.x': [119.69, 124.82],
+ '23.y': [121.41, 113.45],
+ '24.height': [26.54, 24.46],
+ '24.width': [29.15, 26.86],
+ '24.x': [208.58, 206.73],
+ '24.y': [129.22, 120.65],
+ '25.height': [79.56, 73.32],
+ '25.width': [46.45, 42.81],
+ '25.x': [46.17, 57.07],
+ '25.y': [67.32, 63.6],
+ '26.height': [10.13, 9.33],
+ '26.width': [6.07, 5.59],
+ '26.x': [312.2, 302.22],
+ '26.y': [93.42, 87.66],
+ '27.height': [1.46, 1.35],
+ '27.width': [1.21, 1.11],
+ '27.x': [306.11, 296.6],
+ '27.y': [101.26, 94.88],
+ '28.height': [24.14, 22.25],
+ '28.width': [37.35, 34.42],
+ '28.x': [109.83, 115.73],
+ '28.y': [185.85, 172.84],
+ '29.height': [22.99, 21.19],
+ '29.width': [34.93, 32.19],
+ '29.x': [197.18, 196.22],
+ '29.y': [78.05, 73.49],
+ '30.height': [21.99, 20.27],
+ '30.width': [42.87, 39.5],
+ '30.x': [237.88, 233.73],
+ '30.y': [107, 100.17],
+ '31.height': [13.14, 12.11],
+ '31.width': [26.24, 24.18],
+ '31.x': [292.07, 283.66],
+ '31.y': [94.57, 88.72],
+ '32.height': [46.09, 42.47],
+ '32.width': [43.84, 40.4],
+ '32.x': [228.67, 225.24],
+ '32.y': [42.99, 41.18],
+ '33.height': [35.89, 33.07],
+ '33.width': [20.73, 19.11],
+ '33.x': [227.65, 224.31],
+ '33.y': [139.44, 130.07],
+ '34.height': [37.56, 34.61],
+ '34.width': [59.1, 54.46],
+ '34.x': [102.45, 108.93],
+ '34.y': [28.4, 27.74],
+ '35.height': [20.11, 18.53],
+ '35.width': [9.53, 8.78],
+ '35.x': [323.71, 312.82],
+ '35.y': [48.79, 46.53],
+ '36.height': [33.36, 30.74],
+ '36.width': [43.7, 40.27],
+ '36.x': [287.3, 279.27],
+ '36.y': [53.75, 51.1],
+ '37.height': [27.71, 25.53],
+ '37.width': [24.23, 22.33],
+ '37.x': [261.01, 255.04],
+ '37.y': [83.09, 78.14],
+ '38.height': [39.29, 36.21],
+ '38.width': [46.51, 42.86],
+ '38.x': [50.24, 60.82],
+ '38.y': [37.93, 36.52],
+ '39.height': [17.19, 15.84],
+ '39.width': [49.29, 45.42],
+ '39.x': [234.3, 230.43],
+ '39.y': [123.14, 115.05],
+ '40.height': [40.85, 37.65],
+ '40.width': [32.55, 30],
+ '40.x': [96.88, 103.8],
+ '40.y': [80.56, 75.8],
+ '41.height': [25.63, 23.62],
+ '41.width': [45.96, 42.35],
+ '41.x': [271.94, 265.11],
+ '41.y': [99.24, 93.02],
+ '42.height': [28.78, 26.52],
+ '42.width': [38.83, 35.78],
+ '42.x': [60.5, 70.27],
+ '43.height': [33.44, 30.81],
+ '43.width': [31.08, 28.64],
+ '43.x': [216.44, 213.98],
+ '43.y': [51.72, 49.23],
+ '44.height': [23.9, 22.02],
+ '44.width': [47.8, 44.05],
+ '44.x': [156.58, 158.81],
+ '44.y': [80.54, 75.79],
+ '45.height': [22.49, 20.72],
+ '45.width': [29.46, 27.15],
+ '45.x': [275.64, 268.53],
+ '45.y': [133.25, 124.36],
+ '46.height': [55.9, 51.51],
+ '46.width': [34.51, 31.8],
+ '46.x': [88.14, 95.74],
+ '46.y': [27.39, 26.81],
+ '47.height': [57, 52.52],
+ '47.width': [36.84, 33.95],
+ '47.x': [67.05, 76.31],
+ '47.y': [73.5, 69.3],
+ '48.height': [18.39, 16.95],
+ '48.width': [9.78, 9.01],
+ '48.x': [316.3, 306],
+ '48.y': [51.4, 48.94],
+ '49.height': [29.14, 26.85],
+ '49.width': [33.47, 30.85],
+ '49.x': [212.47, 210.31],
+ '49.y': [155.25, 144.63],
+ '50.height': [6.43, 5.93],
+ '50.width': [4.51, 4.15],
+ '50.x': [329.57, 318.22],
+ '50.y': [72.49, 68.37],
+ '51.cx': [204.54, 203.01],
+ '51.cy': [179.76, 167.22],
+ '52.cx': [184.71, 184.73],
+ '52.cy': [182.05, 169.33],
+ '53.cx': [195.67, 194.83],
+ '53.cy': [157.62, 146.82],
+ '54.cx': [189.59, 189.23],
+ '54.cy': [175.95, 163.71],
+ '55.cx': [136.13, 139.96],
+ '55.cy': [161.59, 150.48],
+ },
+ legend: {
+ 'pointCategoricalLegend.bounds.height': [16, 31],
+ 'pointSizeLegend.bounds.height': [71.48, 72],
+ 'pointSizeLegend.bounds.width': [66.79, 69],
+ 'pointSizeLegend.bounds.x': [295.21, 293],
+ 'pointSizeLegend.bounds.y': [266.92, 266.4],
+ 'pointCategoricalLegend.entry.0.y': [undefined, 116.3],
+ 'pointCategoricalLegend.entry.1.y': [undefined, 116.3],
+ 'pointCategoricalLegend.entry.2.y': [undefined, 116.3],
+ },
+ },
+ style: {},
+ },
+ '../gallery/maps.stories.tsx.readerZoomPointsSpec @1280': {
+ reason:
+ 'Approved B3 static map captions and neutral-class context reserve measured key space; occupied-bounds size-key placement has no legacy layout opt-out.',
+ changes: {
+ dims: {
+ 'mapSize.height': [215.2, 200.2],
+ },
+ area: {
+ 'rect.height': [215.2, 200.2],
+ 'rect.y': [121.2, 136.2],
+ },
+ marks: {
+ '0.height': [33.34, 30.49],
+ '0.width': [20.42, 18.68],
+ '0.x': [676.62, 671.77],
+ '0.y': [128.23, 118.97],
+ '1.height': [41.2, 37.67],
+ '1.width': [76.9, 70.32],
+ '1.x': [470.26, 483.08],
+ '1.y': [153.29, 141.88],
+ '2.height': [42.19, 38.58],
+ '2.width': [36.1, 33.01],
+ '2.x': [529.28, 537.04],
+ '2.y': [108.64, 101.05],
+ '3.height': [30.56, 27.94],
+ '3.width': [38.56, 35.26],
+ '3.x': [565.38, 570.05],
+ '3.y': [86.17, 80.51],
+ '4.height': [41.83, 38.25],
+ '4.width': [48.35, 44.21],
+ '4.x': [682.15, 676.83],
+ '4.y': [153.37, 141.95],
+ '5.height': [30.21, 27.63],
+ '5.width': [28.84, 26.38],
+ '5.x': [690.69, 684.63],
+ '5.y': [126.46, 117.35],
+ '6.height': [28.43, 26],
+ '6.width': [16.48, 15.07],
+ '6.x': [675.56, 670.8],
+ '6.y': [82.26, 76.93],
+ '7.height': [21.29, 19.47],
+ '7.width': [39.3, 35.93],
+ '7.x': [602.27, 603.79],
+ '7.y': [96.41, 89.87],
+ '8.height': [30.87, 28.23],
+ '8.width': [19.54, 17.86],
+ '8.x': [750.21, 739.06],
+ '8.y': [29.62, 28.79],
+ '9.height': [10.17, 9.3],
+ '9.width': [18.44, 16.86],
+ '9.x': [744.23, 733.6],
+ '9.y': [61.85, 58.27],
+ '10.height': [39.29, 35.92],
+ '10.width': [34.9, 31.91],
+ '10.x': [628.74, 627.99],
+ '10.y': [34.51, 33.27],
+ '11.height': [16.27, 14.88],
+ '11.width': [7.09, 6.49],
+ '11.x': [737.9, 727.8],
+ '11.y': [76.39, 71.56],
+ '12.height': [20.72, 18.95],
+ '12.width': [46.82, 42.81],
+ '12.x': [697.6, 690.96],
+ '12.y': [109.41, 101.76],
+ '13.height': [21.9, 20.02],
+ '13.width': [34.87, 31.88],
+ '13.x': [596.79, 598.78],
+ '13.y': [35.56, 34.23],
+ '14.height': [24.16, 22.09],
+ '14.width': [45.79, 41.87],
+ '14.x': [596.94, 598.92],
+ '14.y': [116.33, 108.08],
+ '15.height': [20, 18.29],
+ '15.width': [30.87, 28.22],
+ '15.x': [710.43, 702.69],
+ '15.y': [72.88, 68.35],
+ '16.height': [24.82, 22.7],
+ '16.width': [37.05, 33.88],
+ '16.x': [595.09, 597.22],
+ '16.y': [55.81, 52.75],
+ '17.height': [72.88, 66.64],
+ '17.width': [74.33, 67.96],
+ '17.x': [574.13, 578.06],
+ '17.y': [119.68, 111.15],
+ '18.height': [30.85, 28.21],
+ '18.width': [37.04, 33.87],
+ '18.x': [559.15, 564.36],
+ '18.y': [58.1, 54.83],
+ '19.height': [9.04, 8.26],
+ '19.width': [9.16, 8.37],
+ '19.x': [744.39, 733.74],
+ '19.y': [68.62, 64.45],
+ '20.height': [30.81, 28.17],
+ '20.width': [35.37, 32.34],
+ '20.x': [635.43, 634.11],
+ '20.y': [92.78, 86.55],
+ '21.height': [23.88, 21.84],
+ '21.width': [23.81, 21.77],
+ '21.x': [703.67, 696.51],
+ '21.y': [86.76, 81.05],
+ '22.height': [37.74, 34.51],
+ '22.width': [21.2, 19.39],
+ '22.x': [656.99, 653.83],
+ '22.y': [78.86, 73.82],
+ '23.height': [38.49, 35.2],
+ '23.width': [37.08, 33.91],
+ '23.x': [560.13, 565.25],
+ '23.y': [112.97, 105.01],
+ '24.height': [24.33, 22.25],
+ '24.width': [26.72, 24.43],
+ '24.x': [641.62, 639.77],
+ '24.y': [120.13, 111.56],
+ '25.height': [72.94, 66.7],
+ '25.width': [42.59, 38.94],
+ '25.x': [492.72, 503.62],
+ '25.y': [63.38, 59.66],
+ '26.height': [9.29, 8.49],
+ '26.width': [5.56, 5.09],
+ '26.x': [736.62, 726.63],
+ '26.y': [87.32, 81.55],
+ '27.height': [1.34, 1.23],
+ '27.width': [1.11, 1.01],
+ '27.x': [731.03, 721.53],
+ '27.y': [94.5, 88.12],
+ '28.height': [22.13, 20.24],
+ '28.width': [34.24, 31.31],
+ '28.x': [551.08, 556.98],
+ '28.y': [172.05, 159.04],
+ '29.height': [21.08, 19.27],
+ '29.width': [32.03, 29.29],
+ '29.x': [631.16, 630.21],
+ '29.y': [73.22, 68.66],
+ '30.height': [20.16, 18.44],
+ '30.width': [39.3, 35.94],
+ '30.x': [668.48, 664.33],
+ '30.y': [99.76, 92.93],
+ '31.height': [12.04, 11.01],
+ '31.width': [24.06, 22],
+ '31.x': [718.16, 709.75],
+ '31.y': [88.37, 82.51],
+ '32.height': [42.25, 38.64],
+ '32.width': [40.19, 36.75],
+ '32.x': [660.03, 656.61],
+ '32.y': [41.08, 39.27],
+ '33.height': [32.9, 30.09],
+ '33.width': [19.01, 17.38],
+ '33.x': [659.11, 655.76],
+ '33.y': [129.51, 120.13],
+ '34.height': [34.43, 31.48],
+ '34.width': [54.18, 49.54],
+ '34.x': [544.32, 550.8],
+ '34.y': [27.7, 27.04],
+ '35.height': [18.43, 16.86],
+ '35.width': [8.74, 7.99],
+ '35.x': [747.17, 736.28],
+ '35.y': [46.39, 44.13],
+ '36.height': [30.58, 27.96],
+ '36.width': [40.07, 36.64],
+ '36.x': [713.79, 705.76],
+ '36.y': [50.94, 48.29],
+ '37.height': [25.4, 23.23],
+ '37.width': [22.22, 20.31],
+ '37.x': [689.68, 683.72],
+ '37.y': [77.84, 72.89],
+ '38.height': [36.02, 32.94],
+ '38.width': [42.64, 38.99],
+ '38.x': [496.45, 507.03],
+ '38.y': [36.44, 35.03],
+ '39.height': [15.76, 14.41],
+ '39.width': [45.19, 41.32],
+ '39.x': [665.2, 661.33],
+ '39.y': [114.56, 106.46],
+ '40.height': [37.45, 34.25],
+ '40.width': [29.85, 27.29],
+ '40.x': [539.21, 546.13],
+ '40.y': [75.52, 70.76],
+ '41.height': [23.5, 21.48],
+ '41.width': [42.13, 38.53],
+ '41.x': [699.7, 692.88],
+ '41.y': [92.65, 86.43],
+ '42.height': [26.38, 24.12],
+ '42.width': [35.6, 32.55],
+ '42.x': [505.86, 515.63],
+ '43.height': [30.66, 28.03],
+ '43.width': [28.5, 26.06],
+ '43.x': [648.83, 646.36],
+ '43.y': [49.08, 46.59],
+ '44.height': [21.91, 20.03],
+ '44.width': [43.83, 40.07],
+ '44.x': [593.95, 596.18],
+ '44.y': [75.5, 70.75],
+ '45.height': [20.61, 18.85],
+ '45.width': [27.01, 24.7],
+ '45.x': [703.1, 695.99],
+ '45.y': [123.83, 114.94],
+ '46.height': [51.25, 46.86],
+ '46.width': [31.64, 28.93],
+ '46.x': [531.2, 538.8],
+ '46.y': [26.77, 26.19],
+ '47.height': [52.25, 47.78],
+ '47.width': [33.77, 30.88],
+ '47.x': [511.86, 521.12],
+ '47.y': [69.05, 64.85],
+ '48.height': [16.86, 15.42],
+ '48.width': [8.96, 8.19],
+ '48.x': [740.38, 730.07],
+ '48.y': [48.79, 46.32],
+ '49.height': [26.71, 24.43],
+ '49.width': [30.69, 28.06],
+ '49.x': [645.18, 643.03],
+ '49.y': [143.99, 133.38],
+ '50.height': [5.9, 5.39],
+ '50.width': [4.13, 3.78],
+ '50.x': [752.54, 741.2],
+ '50.y': [68.12, 64],
+ '51.cx': [637.92, 636.38],
+ '51.cy': [166.47, 153.93],
+ '52.cx': [619.73, 619.76],
+ '52.cy': [168.57, 155.85],
+ '53.cx': [629.78, 628.94],
+ '53.cy': [146.17, 135.37],
+ '54.cx': [624.21, 623.85],
+ '54.cy': [162.97, 150.73],
+ '55.cx': [575.19, 579.03],
+ '55.cy': [149.81, 138.7],
+ },
+ legend: {
+ 'pointCategoricalLegend.bounds.height': [16, 31],
+ 'pointSizeLegend.bounds.height': [71.48, 72],
+ 'pointSizeLegend.bounds.width': [66.79, 69],
+ 'pointSizeLegend.bounds.x': [1175.21, 1173],
+ 'pointSizeLegend.bounds.y': [246.92, 246.4],
+ 'pointCategoricalLegend.entry.0.y': [undefined, 112.2],
+ 'pointCategoricalLegend.entry.1.y': [undefined, 112.2],
+ 'pointCategoricalLegend.entry.2.y': [undefined, 112.2],
+ },
+ },
+ style: {},
+ },
+ '../gallery/showcase.stories.tsx.worldMapSpec @390': {
+ reason:
+ 'Approved B3 static map captions and neutral-class context reserve measured key space; occupied-bounds size-key placement has no legacy layout opt-out.',
+ changes: {
+ dims: {
+ 'mapSize.height': [149.3, 111.3],
+ },
+ area: {
+ 'rect.height': [149.3, 111.3],
+ 'rect.y': [174.7, 212.7],
+ },
+ marks: {
+ '0.height': [2.55, 1.9],
+ '0.width': [302.38, 225.42],
+ '0.x': [33.81, 72.29],
+ '0.y': [92.66, 69.08],
+ '1.height': [12.37, 9.22],
+ '1.width': [9.17, 6.83],
+ '1.x': [210.08, 203.69],
+ '1.y': [75.41, 56.21],
+ '2.height': [7.3, 5.45],
+ '2.width': [7.13, 5.31],
+ '2.x': [170.86, 174.46],
+ '2.y': [43.08, 32.11],
+ '3.height': [28.47, 21.22],
+ '3.width': [59.19, 44.13],
+ '3.x': [93.72, 116.95],
+ '3.y': [0.09, 0.07],
+ '4.height': [47.9, 35.71],
+ '4.width': [82.99, 61.87],
+ '4.x': [53.03, 86.62],
+ '4.y': [4.81, 3.58],
+ '5.height': [13.51, 10.07],
+ '5.width': [28.89, 21.54],
+ '5.x': [218.18, 209.74],
+ '5.y': [16.05, 11.96],
+ '6.height': [8.31, 6.19],
+ '6.width': [14.09, 10.5],
+ '6.x': [226.03, 215.59],
+ '6.y': [24.79, 18.48],
+ '7.height': [9.37, 6.98],
+ '7.width': [13.14, 9.8],
+ '7.x': [304.99, 274.45],
+ '7.y': [77.19, 57.55],
+ '8.height': [18.23, 13.59],
+ '8.width': [39.24, 29.25],
+ '8.x': [266.39, 245.67],
+ '8.y': [68, 50.69],
+ '9.height': [33.29, 24.82],
+ '9.width': [12.69, 9.46],
+ '9.x': [128.78, 143.09],
+ '9.y': [99.16, 73.93],
+ '10.height': [38.34, 28.58],
+ '10.width': [13.44, 10.02],
+ '10.x': [126.23, 141.19],
+ '10.y': [94.42, 70.39],
+ '11.height': [21.27, 15.86],
+ '11.width': [16.27, 12.13],
+ '11.x': [195.4, 192.75],
+ '11.y': [68.25, 50.88],
+ '12.height': [15.75, 11.74],
+ '12.width': [8.28, 6.17],
+ '12.x': [220.06, 211.14],
+ '12.y': [60.5, 45.1],
+ '13.height': [11.73, 8.75],
+ '13.width': [6.78, 5.05],
+ '13.x': [214.01, 206.63],
+ '13.y': [67.97, 50.67],
+ '14.height': [15.57, 11.61],
+ '14.width': [13.55, 10.1],
+ '14.x': [203.56, 198.84],
+ '14.y': [49.27, 36.73],
+ '15.height': [18.05, 13.46],
+ '15.width': [8.67, 6.46],
+ '15.x': [196.42, 193.51],
+ '15.y': [47.71, 35.57],
+ '16.height': [2.11, 1.57],
+ '16.width': [2.52, 1.88],
+ '16.x': [122.82, 138.65],
+ '16.y': [51.59, 38.46],
+ '17.height': [2.56, 1.91],
+ '17.width': [3.02, 2.25],
+ '17.x': [124.96, 140.24],
+ '17.y': [51.62, 38.48],
+ '18.height': [28.48, 21.23],
+ '18.width': [223.2, 166.39],
+ '18.x': [74.72, 102.79],
+ '18.y': [0.6, 0.45],
+ '19.height': [3.64, 2.71],
+ '19.width': [1.64, 1.22],
+ '19.x': [120.81, 137.15],
+ '19.y': [43.74, 32.61],
+ '20.height': [1.05, 0.79],
+ '20.width': [2.3, 1.71],
+ '20.x': [142.45, 153.28],
+ '20.y': [128.89, 96.08],
+ '21.height': [13.08, 9.75],
+ '21.width': [15, 11.18],
+ '21.x': [188.16, 187.35],
+ '21.y': [0.77, 0.58],
+ '22.height': [12.33, 9.19],
+ '22.width': [32.95, 24.56],
+ '22.x': [145.66, 155.68],
+ '23.height': [1.04, 0.78],
+ '23.width': [1.55, 1.15],
+ '23.x': [233.63, 221.25],
+ '23.y': [126.66, 94.42],
+ '24.height': [1.28, 0.96],
+ '24.width': [2.08, 1.55],
+ '24.x': [291.35, 264.28],
+ '24.y': [83.83, 62.5],
+ '25.height': [13.68, 10.2],
+ '25.width': [13.48, 10.05],
+ '25.x': [198.17, 194.82],
+ '25.y': [99.45, 74.14],
+ '26.height': [2.14, 1.59],
+ '26.width': [1.93, 1.44],
+ '26.x': [206.63, 201.12],
+ '26.y': [106.61, 79.48],
+ '27.height': [19.96, 14.88],
+ '27.width': [21.17, 15.78],
+ '27.x': [91.96, 115.64],
+ '27.y': [37.68, 28.09],
+ '28.height': [5.09, 3.8],
+ '28.width': [4.16, 3.1],
+ '28.x': [138.88, 150.62],
+ '28.y': [108.18, 80.65],
+ '29.height': [43.77, 32.63],
+ '29.width': [33.46, 24.94],
+ '29.x': [121.93, 137.98],
+ '29.y': [68.27, 50.89],
+ '30.height': [14.78, 11.02],
+ '30.width': [10.96, 8.17],
+ '30.x': [126, 141.02],
+ '30.y': [85.54, 63.77],
+ '31.height': [20.9, 15.58],
+ '31.width': [11.71, 8.73],
+ '31.x': [115.43, 133.14],
+ '31.y': [74.38, 55.45],
+ '32.height': [19.24, 14.34],
+ '32.width': [10.36, 7.72],
+ '32.x': [117.4, 134.61],
+ '32.y': [60.03, 44.75],
+ '33.height': [2.74, 2.04],
+ '33.width': [4.85, 3.62],
+ '33.x': [114.33, 132.32],
+ '33.y': [63.25, 47.15],
+ '34.height': [3.42, 2.55],
+ '34.width': [2.74, 2.04],
+ '34.x': [112.07, 130.63],
+ '34.y': [61.42, 45.79],
+ '35.height': [4.88, 3.64],
+ '35.width': [4.12, 3.07],
+ '35.x': [110.87, 129.73],
+ '35.y': [57.1, 42.56],
+ '36.height': [3.43, 2.55],
+ '36.width': [5.32, 3.97],
+ '36.x': [109.67, 128.84],
+ '36.y': [55.98, 41.73],
+ '37.height': [1.45, 1.08],
+ '37.width': [2.01, 1.5],
+ '37.x': [108.94, 128.3],
+ '37.y': [57.77, 43.07],
+ '38.height': [4.62, 3.44],
+ '38.width': [3.57, 2.66],
+ '38.x': [107.27, 127.05],
+ '38.y': [53.94, 40.21],
+ '39.height': [2.94, 2.19],
+ '39.width': [1.4, 1.05],
+ '39.x': [110.02, 129.11],
+ '39.y': [53.17, 39.64],
+ '40.height': [13.13, 9.79],
+ '40.width': [11.47, 8.55],
+ '40.x': [122.64, 138.51],
+ '40.y': [60.34, 44.98],
+ '41.height': [8.17, 6.09],
+ '41.width': [4.05, 3.02],
+ '41.x': [132.57, 145.91],
+ '41.y': [64.68, 48.22],
+ '42.height': [4.85, 3.61],
+ '42.width': [3.55, 2.65],
+ '42.x': [135.38, 148.01],
+ '42.y': [67.37, 50.22],
+ '43.height': [52.25, 38.95],
+ '43.width': [53.8, 40.11],
+ '43.x': [138.35, 150.22],
+ '43.y': [19.69, 14.68],
+ '44.height': [7.31, 5.45],
+ '44.width': [4.88, 3.64],
+ '44.x': [115.72, 133.35],
+ '44.y': [72.72, 54.21],
+ '45.height': [0.64, 0.48],
+ '45.width': [1.37, 1.02],
+ '45.x': [128.83, 143.13],
+ '45.y': [53.15, 39.62],
+ '46.height': [0.92, 0.69],
+ '46.width': [1.73, 1.29],
+ '46.x': [119.56, 136.21],
+ '46.y': [53.14, 39.62],
+ '47.height': [3.7, 2.76],
+ '47.width': [8.61, 6.42],
+ '47.x': [114.79, 132.66],
+ '47.y': [47.96, 35.75],
+ '48.height': [7.57, 5.65],
+ '48.width': [6.42, 4.78],
+ '48.x': [206.13, 200.75],
+ '48.y': [92.08, 68.64],
+ '49.height': [10.14, 7.56],
+ '49.width': [8.03, 5.98],
+ '49.x': [201.28, 197.13],
+ '49.y': [94.51, 70.45],
+ '50.height': [13.34, 9.95],
+ '50.width': [11.16, 8.32],
+ '50.x': [194.83, 192.33],
+ '50.y': [93.7, 69.85],
+ '51.height': [4.84, 3.6],
+ '51.width': [5.15, 3.84],
+ '51.x': [170.15, 173.93],
+ '51.y': [55.31, 41.23],
+ '52.height': [16.7, 12.45],
+ '52.width': [13.84, 10.31],
+ '52.x': [174.74, 177.35],
+ '52.y': [45.99, 34.29],
+ '53.height': [14.19, 10.58],
+ '53.width': [10.11, 7.54],
+ '53.x': [170.86, 174.46],
+ '53.y': [43.36, 32.32],
+ '54.height': [6.98, 5.2],
+ '54.width': [2.57, 1.91],
+ '54.x': [185.66, 185.49],
+ '54.y': [60.26, 44.92],
+ '55.height': [13.27, 9.89],
+ '55.width': [12.95, 9.66],
+ '55.x': [185.25, 185.19],
+ '55.y': [47.64, 35.52],
+ '56.height': [11.02, 8.22],
+ '56.width': [10.05, 7.49],
+ '56.x': [187.3, 186.71],
+ '56.y': [58.4, 43.54],
+ '57.height': [12.77, 9.52],
+ '57.width': [6.45, 4.81],
+ '57.x': [192.25, 190.41],
+ '57.y': [59.55, 44.39],
+ '58.height': [5.83, 4.35],
+ '58.width': [1.63, 1.22],
+ '58.x': [184.96, 184.97],
+ '58.y': [61.64, 45.95],
+ '59.height': [7.33, 5.46],
+ '59.width': [3.68, 2.74],
+ '59.x': [182.23, 182.93],
+ '59.y': [61.55, 45.89],
+ '60.height': [7.1, 5.29],
+ '60.width': [5.16, 3.85],
+ '60.x': [177.66, 179.53],
+ '60.y': [62.21, 46.38],
+ '61.height': [6.04, 4.5],
+ '61.width': [6.17, 4.6],
+ '61.x': [172.16, 175.43],
+ '61.y': [59.86, 44.62],
+ '62.height': [1.81, 1.35],
+ '62.width': [2.52, 1.88],
+ '62.x': [170.88, 174.48],
+ '62.y': [59.81, 44.59],
+ '63.height': [4.81, 3.59],
+ '63.width': [3.32, 2.47],
+ '63.x': [175.24, 177.73],
+ '63.y': [64.48, 48.07],
+ '64.height': [3.74, 2.79],
+ '64.width': [2.56, 1.91],
+ '64.x': [173.73, 176.6],
+ '64.y': [62.75, 46.78],
+ '65.height': [6.27, 4.67],
+ '65.width': [6.48, 4.83],
+ '65.x': [180.36, 181.54],
+ '65.y': [56.98, 42.48],
+ '66.height': [10.19, 7.6],
+ '66.width': [11.03, 8.23],
+ '66.x': [197.35, 194.21],
+ '66.y': [61.5, 45.85],
+ '67.height': [10.1, 7.53],
+ '67.width': [6.3, 4.69],
+ '67.x': [194.48, 192.07],
+ '67.y': [70.01, 52.19],
+ '68.height': [7.27, 5.42],
+ '68.width': [4.82, 3.59],
+ '68.x': [192.53, 190.61],
+ '68.y': [71.63, 53.4],
+ '69.height': [1.47, 1.1],
+ '69.width': [1.69, 1.26],
+ '69.x': [192.96, 190.94],
+ '69.y': [71.68, 53.43],
+ '70.height': [11.05, 8.24],
+ '70.width': [10.05, 7.49],
+ '70.x': [203.38, 198.7],
+ '70.y': [83.79, 62.47],
+ '71.height': [8.61, 6.42],
+ '71.width': [2.48, 1.85],
+ '71.x': [212.6, 205.57],
+ '71.y': [84.93, 63.31],
+ '72.height': [18.38, 13.7],
+ '72.width': [8.93, 6.66],
+ '72.x': [210.42, 203.95],
+ '72.y': [86.17, 64.24],
+ '73.height': [1.77, 1.32],
+ '73.width': [1.13, 0.84],
+ '73.x': [209.91, 203.57],
+ '73.y': [103.38, 77.07],
+ '74.height': [15.38, 11.47],
+ '74.width': [10.62, 7.92],
+ '74.x': [194.76, 192.28],
+ '74.y': [79.42, 59.21],
+ '75.height': [2.48, 1.85],
+ '75.width': [1.47, 1.1],
+ '75.x': [209.83, 203.51],
+ '75.y': [77.02, 57.42],
+ '76.height': [4, 2.98],
+ '76.width': [1.02, 0.76],
+ '76.x': [212.28, 205.34],
+ '76.y': [37.09, 27.65],
+ '77.height': [1.62, 1.21],
+ '77.width': [1.01, 0.75],
+ '77.x': [212.71, 205.66],
+ '77.y': [35.67, 26.59],
+ '78.height': [15.17, 11.31],
+ '78.width': [6.77, 5.05],
+ '78.x': [220.71, 211.62],
+ '78.y': [88.14, 65.71],
+ '79.height': [1.25, 0.93],
+ '79.width': [0.46, 0.35],
+ '79.x': [212.76, 205.69],
+ '79.y': [37.87, 28.23],
+ '80.height': [0.85, 0.63],
+ '80.width': [2.54, 1.9],
+ '80.x': [170.76, 174.39],
+ '80.y': [58.39, 43.53],
+ '81.height': [7.34, 5.48],
+ '81.width': [3.16, 2.35],
+ '81.x': [190.91, 189.4],
+ '81.y': [32.89, 24.52],
+ '82.height': [19.43, 14.48],
+ '82.width': [16.9, 12.6],
+ '82.x': [177.97, 179.76],
+ '82.y': [33.12, 24.69],
+ '83.height': [4.43, 3.3],
+ '83.width': [3.04, 2.27],
+ '83.x': [213.02, 205.89],
+ '83.y': [36.98, 27.57],
+ '84.height': [3.91, 2.91],
+ '84.width': [3.83, 2.86],
+ '84.x': [227.28, 216.52],
+ '84.y': [44.81, 33.41],
+ '85.height': [1.7, 1.27],
+ '85.width': [0.75, 0.56],
+ '85.x': [226.4, 215.86],
+ '85.y': [44.75, 33.36],
+ '86.height': [1.64, 1.23],
+ '86.width': [1.58, 1.18],
+ '86.x': [222.43, 212.91],
+ '86.y': [40.5, 30.19],
+ '87.height': [8.67, 6.47],
+ '87.width': [8.34, 6.22],
+ '87.x': [215.56, 207.78],
+ '87.y': [32.85, 24.49],
+ '88.height': [10.81, 8.06],
+ '88.width': [6, 4.48],
+ '88.x': [228.35, 217.31],
+ '88.y': [44.44, 33.13],
+ '89.height': [2.23, 1.66],
+ '89.width': [0.65, 0.49],
+ '89.x': [325.2, 289.51],
+ '89.y': [91.08, 67.9],
+ '90.height': [4.65, 3.47],
+ '90.width': [4.53, 3.37],
+ '90.x': [271.46, 249.46],
+ '90.y': [57.6, 42.94],
+ '91.height': [16.73, 12.47],
+ '91.width': [7.62, 5.68],
+ '91.x': [266.3, 245.6],
+ '91.y': [51.03, 38.04],
+ '92.height': [9.63, 7.18],
+ '92.width': [7.46, 5.56],
+ '92.x': [268.11, 246.96],
+ '92.y': [48.76, 36.35],
+ '93.height': [20.54, 15.31],
+ '93.width': [7.99, 5.96],
+ '93.x': [261.37, 241.94],
+ '93.y': [42.34, 31.57],
+ '94.height': [16.64, 12.4],
+ '94.width': [8.28, 6.17],
+ '94.x': [269.27, 247.82],
+ '94.y': [47.77, 35.62],
+ '95.height': [5.28, 3.94],
+ '95.width': [3.92, 2.92],
+ '95.x': [279.31, 255.31],
+ '95.y': [27.28, 20.34],
+ '96.height': [4.32, 3.22],
+ '96.width': [3.75, 2.79],
+ '96.x': [282.03, 257.33],
+ '96.y': [31.61, 23.56],
+ '97.height': [9.74, 7.26],
+ '97.width': [24.39, 18.18],
+ '97.x': [247.31, 231.45],
+ '97.y': [18.9, 14.09],
+ '98.height': [30.35, 22.63],
+ '98.width': [22.76, 16.97],
+ '98.x': [240.99, 226.74],
+ '98.y': [34.79, 25.93],
+ '99.height': [6.36, 4.74],
+ '99.width': [4.81, 3.58],
+ '99.x': [256.89, 238.59],
+ '99.y': [44.39, 33.09],
+ '100.height': [1.71, 1.27],
+ '100.width': [2.75, 2.05],
+ '100.x': [256.94, 238.63],
+ '100.y': [42.38, 31.6],
+ '101.height': [4.33, 3.23],
+ '101.width': [7.12, 5.31],
+ '101.x': [249.47, 233.06],
+ '101.y': [40.11, 29.9],
+ '102.height': [14.3, 10.66],
+ '102.width': [11.85, 8.84],
+ '102.x': [233.78, 221.36],
+ '102.y': [33.11, 24.68],
+ '103.height': [9.55, 7.12],
+ '103.width': [10.4, 7.76],
+ '103.x': [232.61, 220.49],
+ '103.y': [31.73, 23.66],
+ '104.height': [4.25, 3.17],
+ '104.width': [6.34, 4.72],
+ '104.x': [236.44, 223.34],
+ '104.y': [29.26, 21.81],
+ '105.height': [3.96, 2.95],
+ '105.width': [7.23, 5.39],
+ '105.x': [237.74, 224.31],
+ '105.y': [26.98, 20.11],
+ '106.height': [7.51, 5.6],
+ '106.width': [11.9, 8.87],
+ '106.x': [224.38, 214.35],
+ '106.y': [27.51, 20.51],
+ '107.height': [15.38, 11.46],
+ '107.width': [17.82, 13.28],
+ '107.x': [218.58, 210.03],
+ '107.y': [30.5, 22.74],
+ '108.height': [5.1, 3.8],
+ '108.width': [4.69, 3.49],
+ '108.x': [212.98, 205.86],
+ '108.y': [33.01, 24.61],
+ '109.height': [2.5, 1.86],
+ '109.width': [2.71, 2.02],
+ '109.x': [217.84, 209.48],
+ '109.y': [28.98, 21.6],
+ '110.height': [9.96, 7.43],
+ '110.width': [7.29, 5.44],
+ '110.x': [192.19, 190.36],
+ '110.y': [6.1, 4.55],
+ '111.height': [4.14, 3.08],
+ '111.width': [6.4, 4.77],
+ '111.x': [201.04, 196.96],
+ '111.y': [15.4, 11.48],
+ '112.height': [6.42, 4.78],
+ '112.width': [12.78, 9.53],
+ '112.x': [200.79, 196.77],
+ '112.y': [18.65, 13.9],
+ '113.height': [5.1, 3.8],
+ '113.width': [7.21, 5.38],
+ '113.x': [194.66, 192.2],
+ '113.y': [16.5, 12.3],
+ '114.height': [2.41, 1.79],
+ '114.width': [5.32, 3.97],
+ '114.x': [191.85, 190.11],
+ '114.y': [21.58, 16.09],
+ '115.height': [2.66, 1.98],
+ '115.width': [4.58, 3.42],
+ '115.x': [196.72, 193.74],
+ '115.y': [21.96, 16.37],
+ '116.height': [2.77, 2.07],
+ '116.width': [2.74, 2.04],
+ '116.x': [204.06, 199.21],
+ '116.y': [22.1, 16.48],
+ '117.height': [4.27, 3.18],
+ '117.width': [7.02, 5.23],
+ '117.x': [199.71, 195.97],
+ '117.y': [22.33, 16.65],
+ '118.height': [2.06, 1.54],
+ '118.width': [3.85, 2.87],
+ '118.x': [199.11, 195.52],
+ '118.y': [15.23, 11.36],
+ '119.height': [1.91, 1.43],
+ '119.width': [4.82, 3.59],
+ '119.x': [199.03, 195.46],
+ '119.y': [13.94, 10.4],
+ '120.height': [1.68, 1.25],
+ '120.width': [3.09, 2.31],
+ '120.x': [200.17, 196.31],
+ '120.y': [12.66, 9.44],
+ '121.height': [6.79, 5.06],
+ '121.width': [6.34, 4.73],
+ '121.x': [189.16, 188.1],
+ '121.y': [16.38, 12.21],
+ '122.height': [2.92, 2.18],
+ '122.width': [4.58, 3.42],
+ '122.x': [201.56, 197.34],
+ '122.y': [26.07, 19.44],
+ '123.height': [6.97, 5.2],
+ '123.width': [5.18, 3.86],
+ '123.x': [200.32, 196.42],
+ '123.y': [28.41, 21.18],
+ '124.height': [6.35, 4.73],
+ '124.width': [14.97, 11.16],
+ '124.x': [204.58, 199.6],
+ '124.y': [28.1, 20.95],
+ '125.height': [3.02, 2.25],
+ '125.width': [1.43, 1.07],
+ '125.x': [199.44, 195.76],
+ '125.y': [27.57, 20.55],
+ '126.height': [3.85, 2.87],
+ '126.width': [4.2, 3.13],
+ '126.x': [195, 192.45],
+ '126.y': [23.92, 17.83],
+ '127.height': [1.92, 1.43],
+ '127.width': [3.18, 2.37],
+ '127.x': [189.37, 188.26],
+ '127.y': [22.69, 16.91],
+ '128.height': [0.62, 0.46],
+ '128.width': [0.39, 0.29],
+ '128.x': [189.02, 188],
+ '128.y': [20.6, 15.36],
+ '129.height': [1.74, 1.29],
+ '129.width': [2.56, 1.91],
+ '129.x': [186.76, 186.31],
+ '129.y': [19.4, 14.46],
+ '130.height': [2.36, 1.76],
+ '130.width': [2.56, 1.91],
+ '130.x': [187.31, 186.72],
+ '130.y': [17.63, 13.14],
+ '131.height': [5.44, 4.06],
+ '131.width': [2.48, 1.85],
+ '131.x': [177.72, 179.57],
+ '131.y': [27.96, 20.85],
+ '132.height': [7.78, 5.8],
+ '132.width': [9.27, 6.91],
+ '132.x': [178.01, 179.79],
+ '132.y': [26.54, 19.79],
+ '133.height': [2.97, 2.21],
+ '133.width': [2.79, 2.08],
+ '133.x': [178.06, 179.83],
+ '133.y': [16.26, 12.12],
+ '134.height': [2.55, 1.9],
+ '134.width': [1.7, 1.27],
+ '134.x': [321.27, 286.59],
+ '134.y': [97.24, 72.49],
+ '135.height': [4.85, 3.61],
+ '135.width': [4.32, 3.22],
+ '135.x': [318.5, 284.52],
+ '135.y': [81.91, 61.06],
+ '136.height': [12.07, 9],
+ '136.width': [16.14, 12.03],
+ '136.x': [306.19, 275.34],
+ '136.y': [112.75, 84.06],
+ '137.height': [35.4, 26.39],
+ '137.width': [34.59, 25.79],
+ '137.x': [275.22, 252.26],
+ '137.y': [86.57, 64.54],
+ '138.height': [4.42, 3.3],
+ '138.width': [1.84, 1.37],
+ '138.x': [252.88, 235.61],
+ '138.y': [63.01, 46.97],
+ '139.height': [35.83, 26.71],
+ '139.width': [42.15, 31.42],
+ '139.x': [241.04, 226.78],
+ '139.y': [17.68, 13.18],
+ '140.height': [3.66, 2.73],
+ '140.width': [1.14, 0.85],
+ '140.x': [283.69, 258.57],
+ '140.y': [45.64, 34.03],
+ '141.height': [10.28, 7.66],
+ '141.width': [9.06, 6.75],
+ '141.x': [189.95, 188.69],
+ '141.y': [23.35, 17.41],
+ '142.height': [2.4, 1.79],
+ '142.width': [3.14, 2.34],
+ '142.x': [190.39, 189.02],
+ '142.y': [14.14, 10.54],
+ '143.height': [7.33, 5.46],
+ '143.width': [6.33, 4.72],
+ '143.x': [179.84, 181.15],
+ '143.y': [13.42, 10],
+ '144.height': [2.06, 1.54],
+ '144.width': [6.48, 4.83],
+ '144.x': [170.19, 173.96],
+ '144.y': [7.72, 5.76],
+ '145.height': [3.58, 2.67],
+ '145.width': [4.32, 3.22],
+ '145.x': [218.85, 210.23],
+ '145.y': [28.38, 21.15],
+ '146.height': [2.43, 1.81],
+ '146.width': [5.48, 4.09],
+ '146.x': [214.64, 207.09],
+ '146.y': [26.73, 19.93],
+ '147.height': [14.71, 10.97],
+ '147.width': [8.12, 6.05],
+ '147.x': [284.79, 259.39],
+ '147.y': [53.17, 39.63],
+ '148.height': [7.09, 5.28],
+ '148.width': [16.39, 12.22],
+ '148.x': [270.41, 248.67],
+ '148.y': [66.33, 49.45],
+ '149.height': [1.66, 1.24],
+ '149.width': [1, 0.74],
+ '149.x': [282.61, 257.77],
+ '149.y': [68.03, 50.72],
+ '150.height': [1.32, 0.98],
+ '150.width': [2.05, 1.53],
+ '150.x': [194.97, 192.44],
+ '150.y': [23.6, 17.59],
+ '151.height': [7, 5.22],
+ '151.width': [7.63, 5.69],
+ '151.x': [197.11, 194.02],
+ '151.y': [5.48, 4.09],
+ '152.height': [1.66, 1.24],
+ '152.width': [3.98, 2.97],
+ '152.x': [197.06, 193.99],
+ '152.y': [21.1, 15.73],
+ '153.height': [2.31, 1.72],
+ '153.width': [4.74, 3.54],
+ '153.x': [193.62, 191.43],
+ '153.y': [19.72, 14.7],
+ '154.height': [6.27, 4.67],
+ '154.width': [5.85, 4.36],
+ '154.x': [215.6, 207.81],
+ '154.y': [53.74, 40.06],
+ '155.height': [14.64, 10.91],
+ '155.width': [7.48, 5.58],
+ '155.x': [286.98, 261.02],
+ '155.y': [24.82, 18.5],
+ '156.height': [9.04, 6.74],
+ '156.width': [7.29, 5.43],
+ '156.x': [133.26, 146.43],
+ '156.y': [96.39, 71.86],
+ '157.height': [7.25, 5.4],
+ '157.width': [8.69, 6.48],
+ '157.x': [220.86, 211.73],
+ '157.y': [52.61, 39.22],
+ '158.height': [17.33, 12.92],
+ '158.width': [18.02, 13.43],
+ '158.x': [212.96, 205.85],
+ '158.y': [38.27, 28.53],
+ '159.height': [10.62, 7.92],
+ '159.width': [190.07, 141.7],
+ '159.x': [92.62, 116.13],
+ '159.y': [138.68, 103.38],
+ '160.height': [0.69, 0.52],
+ '160.width': [1.36, 1.01],
+ '160.x': [210.55, 204.04],
+ '160.y': [34.6, 25.8],
+ '161.height': [0.62, 0.47],
+ '161.width': [1.38, 1.03],
+ '161.x': [210.18, 203.77],
+ '161.y': [35.12, 26.18],
+ '162.height': [15.4, 11.48],
+ '162.width': [13.2, 9.84],
+ '162.x': [170.92, 174.5],
+ '162.y': [34.51, 25.73],
+ '163.height': [10.4, 7.75],
+ '163.width': [10.69, 7.97],
+ '163.x': [204.76, 199.73],
+ '163.y': [38.87, 28.98],
+ '164.height': [14.72, 10.98],
+ '164.width': [13.2, 9.84],
+ '164.x': [192.58, 190.65],
+ '164.y': [37.24, 27.76],
+ '165.height': [13.2, 9.84],
+ '165.width': [12.63, 9.42],
+ '165.x': [213.08, 205.93],
+ '165.y': [57.16, 42.61],
+ '166.height': [2.02, 1.51],
+ '166.width': [1.35, 1.01],
+ '166.x': [220.31, 211.32],
+ '166.y': [59.73, 44.53],
+ '167.height': [3.97, 2.96],
+ '167.width': [5.48, 4.09],
+ '167.x': [221.13, 211.94],
+ '167.y': [61.14, 45.58],
+ '168.height': [6.56, 4.89],
+ '168.width': [4.67, 3.48],
+ '168.x': [210.32, 203.87],
+ '168.y': [69.41, 51.74],
+ '169.height': [2.05, 1.53],
+ '169.width': [1.54, 1.15],
+ '169.x': [209.83, 203.51],
+ '169.y': [75.62, 56.37],
+ '170.height': [2.49, 1.85],
+ '170.width': [2.91, 2.17],
+ '170.x': [196.57, 193.62],
+ '170.y': [25.12, 18.73],
+ '171.height': [1.45, 1.08],
+ '171.width': [1.89, 1.41],
+ '171.x': [200.38, 196.46],
+ '171.y': [27.93, 20.82],
+ '172.height': [3.77, 2.81],
+ '172.width': [3.35, 2.5],
+ '172.x': [198.72, 195.23],
+ '172.y': [24.23, 18.07],
+ '173.height': [1.6, 1.19],
+ '173.width': [1.37, 1.02],
+ '173.x': [198.77, 195.27],
+ '173.y': [26.76, 19.95],
+ '174.height': [1.39, 1.04],
+ '174.width': [1.26, 0.94],
+ '174.x': [199.97, 196.16],
+ '174.y': [27, 20.13],
+ '175.height': [1.02, 0.76],
+ '175.width': [0.96, 0.71],
+ '175.x': [132.36, 145.76],
+ '175.y': [61.79, 46.06],
+ '176.height': [10.02, 7.47],
+ '176.width': [9.81, 7.31],
+ '176.x': [205.34, 200.16],
+ '176.y': [60.24, 44.91],
+ },
+ legend: {
+ 'continuousLegend.bounds.height': [61, 99],
+ 'continuousLegend.bar.y': [128.7, 143.7],
+ },
+ },
+ style: {
+ 'legends.continuousLegend.title': ['GDP per capita (US$) ($)', 'GDP per capita (US$)'],
+ },
+ },
+ '../gallery/showcase.stories.tsx.worldMapSpec @1280': {
+ reason:
+ 'Approved B3 static map captions and neutral-class context reserve measured key space; occupied-bounds size-key placement has no legacy layout opt-out.',
+ changes: {
+ dims: {
+ 'mapSize.height': [162, 124],
+ },
+ area: {
+ 'rect.height': [162, 124],
+ 'rect.y': [153.2, 191.2],
+ },
+ marks: {
+ '0.height': [2.77, 2.12],
+ '0.width': [328.1, 251.14],
+ '0.x': [455.95, 494.43],
+ '0.y': [100.54, 76.96],
+ '1.height': [13.42, 10.27],
+ '1.width': [9.95, 7.61],
+ '1.x': [647.21, 640.83],
+ '1.y': [81.82, 62.63],
+ '2.height': [7.93, 6.07],
+ '2.width': [7.74, 5.92],
+ '2.x': [604.66, 608.26],
+ '2.y': [46.74, 35.78],
+ '3.height': [30.89, 23.64],
+ '3.width': [64.23, 49.16],
+ '3.x': [520.95, 544.18],
+ '3.y': [0.1, 0.07],
+ '4.height': [51.97, 39.78],
+ '4.width': [90.05, 68.93],
+ '4.x': [476.8, 510.39],
+ '4.y': [5.22, 3.99],
+ '5.height': [14.66, 11.22],
+ '5.width': [31.35, 24],
+ '5.x': [656, 647.56],
+ '5.y': [17.42, 13.33],
+ '6.height': [9.01, 6.9],
+ '6.width': [15.29, 11.7],
+ '6.x': [664.52, 654.08],
+ '6.y': [26.89, 20.59],
+ '7.height': [10.16, 7.78],
+ '7.width': [14.26, 10.92],
+ '7.x': [750.2, 719.66],
+ '7.y': [83.76, 64.11],
+ '8.height': [19.78, 15.14],
+ '8.width': [42.58, 32.59],
+ '8.x': [708.31, 687.6],
+ '8.y': [73.78, 56.47],
+ '9.height': [36.13, 27.65],
+ '9.width': [13.77, 10.54],
+ '9.x': [559, 573.31],
+ '9.y': [107.6, 82.36],
+ '10.height': [41.61, 31.85],
+ '10.width': [14.58, 11.16],
+ '10.x': [556.23, 571.19],
+ '10.y': [102.45, 78.42],
+ '11.height': [23.08, 17.67],
+ '11.width': [17.66, 13.52],
+ '11.x': [631.29, 628.64],
+ '11.y': [74.06, 56.69],
+ '12.height': [17.09, 13.08],
+ '12.width': [8.98, 6.87],
+ '12.x': [658.04, 649.12],
+ '12.y': [65.64, 50.25],
+ '13.height': [12.73, 9.75],
+ '13.width': [7.35, 5.63],
+ '13.x': [651.48, 644.09],
+ '13.y': [73.75, 56.45],
+ '14.height': [16.89, 12.93],
+ '14.width': [14.7, 11.25],
+ '14.x': [640.14, 635.42],
+ '14.y': [53.46, 40.92],
+ '15.height': [19.59, 14.99],
+ '15.width': [9.41, 7.2],
+ '15.x': [632.39, 629.48],
+ '15.y': [51.77, 39.63],
+ '16.height': [2.29, 1.75],
+ '16.width': [2.74, 2.1],
+ '16.x': [552.53, 568.36],
+ '16.y': [55.98, 42.85],
+ '17.height': [2.78, 2.13],
+ '17.width': [3.28, 2.51],
+ '17.x': [554.86, 570.14],
+ '17.y': [56.01, 42.87],
+ '18.height': [30.9, 23.65],
+ '18.width': [242.18, 185.37],
+ '18.x': [500.34, 528.41],
+ '18.y': [0.65, 0.5],
+ '19.height': [3.95, 3.02],
+ '19.width': [1.78, 1.36],
+ '19.x': [550.35, 566.69],
+ '19.y': [47.47, 36.33],
+ '20.height': [1.14, 0.88],
+ '20.width': [2.49, 1.91],
+ '20.x': [573.83, 584.66],
+ '20.y': [139.85, 107.05],
+ '21.height': [14.2, 10.87],
+ '21.width': [16.27, 12.45],
+ '21.x': [623.42, 622.62],
+ '21.y': [0.84, 0.64],
+ '22.height': [13.38, 10.24],
+ '22.width': [35.75, 27.36],
+ '22.x': [577.32, 587.33],
+ '23.height': [1.13, 0.87],
+ '23.width': [1.68, 1.28],
+ '23.x': [672.76, 660.39],
+ '23.y': [137.43, 105.2],
+ '24.height': [1.39, 1.07],
+ '24.width': [2.26, 1.73],
+ '24.x': [735.4, 708.33],
+ '24.y': [90.96, 69.63],
+ '25.height': [14.85, 11.36],
+ '25.width': [14.63, 11.2],
+ '25.x': [634.29, 630.94],
+ '25.y': [107.91, 82.6],
+ '26.height': [2.32, 1.77],
+ '26.width': [2.09, 1.6],
+ '26.x': [643.47, 637.96],
+ '26.y': [115.68, 88.55],
+ '27.height': [21.66, 16.58],
+ '27.width': [22.97, 17.58],
+ '27.x': [519.05, 542.73],
+ '27.y': [40.88, 31.29],
+ '28.height': [5.53, 4.23],
+ '28.width': [4.52, 3.46],
+ '28.x': [569.96, 581.7],
+ '28.y': [117.38, 89.85],
+ '29.height': [47.5, 36.36],
+ '29.width': [36.3, 27.79],
+ '29.x': [551.57, 567.62],
+ '29.y': [74.07, 56.7],
+ '30.height': [16.03, 12.27],
+ '30.width': [11.89, 9.1],
+ '30.x': [555.99, 571],
+ '30.y': [92.82, 71.04],
+ '31.height': [22.68, 17.36],
+ '31.width': [12.7, 9.72],
+ '31.x': [544.51, 562.22],
+ '31.y': [80.7, 61.77],
+ '32.height': [20.87, 15.98],
+ '32.width': [11.24, 8.61],
+ '32.x': [546.65, 563.86],
+ '32.y': [65.13, 49.85],
+ '33.height': [2.97, 2.28],
+ '33.width': [5.26, 4.03],
+ '33.x': [543.32, 561.31],
+ '33.y': [68.64, 52.54],
+ '34.height': [3.72, 2.84],
+ '34.width': [2.97, 2.28],
+ '34.x': [540.87, 559.43],
+ '34.y': [66.64, 51.01],
+ '35.height': [5.3, 4.05],
+ '35.width': [4.47, 3.42],
+ '35.x': [539.56, 558.43],
+ '35.y': [61.95, 47.42],
+ '36.height': [3.72, 2.84],
+ '36.width': [5.77, 4.42],
+ '36.x': [538.26, 557.44],
+ '36.y': [60.74, 46.49],
+ '37.height': [1.57, 1.2],
+ '37.width': [2.18, 1.67],
+ '37.x': [537.47, 556.83],
+ '37.y': [62.68, 47.98],
+ '38.height': [5.01, 3.83],
+ '38.width': [3.87, 2.97],
+ '38.x': [535.66, 555.44],
+ '38.y': [58.52, 44.8],
+ '39.height': [3.19, 2.44],
+ '39.width': [1.52, 1.16],
+ '39.x': [538.64, 557.73],
+ '39.y': [57.69, 44.16],
+ '40.height': [14.25, 10.91],
+ '40.width': [12.45, 9.53],
+ '40.x': [552.33, 568.2],
+ '40.y': [65.47, 50.12],
+ '41.height': [8.86, 6.78],
+ '41.width': [4.39, 3.36],
+ '41.x': [563.11, 576.45],
+ '41.y': [70.18, 53.72],
+ '42.height': [5.26, 4.02],
+ '42.width': [3.85, 2.95],
+ '42.x': [566.15, 578.78],
+ '42.y': [73.1, 55.95],
+ '43.height': [56.7, 43.4],
+ '43.width': [58.38, 44.69],
+ '43.x': [569.38, 581.25],
+ '43.y': [21.36, 16.35],
+ '44.height': [7.93, 6.07],
+ '44.width': [5.3, 4.06],
+ '44.x': [544.83, 562.46],
+ '44.y': [78.9, 60.39],
+ '45.height': [0.7, 0.53],
+ '45.width': [1.49, 1.14],
+ '45.x': [559.06, 573.35],
+ '45.y': [57.67, 44.14],
+ '46.height': [1, 0.77],
+ '46.width': [1.88, 1.44],
+ '46.x': [548.99, 565.65],
+ '46.y': [57.67, 44.14],
+ '47.height': [4.01, 3.07],
+ '47.width': [9.34, 7.15],
+ '47.x': [543.81, 561.68],
+ '47.y': [52.04, 39.83],
+ '48.height': [8.22, 6.29],
+ '48.width': [6.96, 5.33],
+ '48.x': [642.93, 637.55],
+ '48.y': [99.91, 76.48],
+ '49.height': [11, 8.42],
+ '49.width': [8.71, 6.67],
+ '49.x': [637.66, 633.52],
+ '49.y': [102.55, 78.49],
+ '50.height': [14.48, 11.08],
+ '50.width': [12.11, 9.27],
+ '50.x': [630.66, 628.16],
+ '50.y': [101.67, 77.82],
+ '51.height': [5.25, 4.02],
+ '51.width': [5.59, 4.28],
+ '51.x': [603.89, 607.67],
+ '51.y': [60.02, 45.94],
+ '52.height': [18.13, 13.87],
+ '52.width': [15.01, 11.49],
+ '52.x': [608.87, 611.48],
+ '52.y': [49.91, 38.2],
+ '53.height': [15.4, 11.79],
+ '53.width': [10.97, 8.4],
+ '53.x': [604.66, 608.26],
+ '53.y': [47.05, 36.01],
+ '54.height': [7.57, 5.8],
+ '54.width': [2.78, 2.13],
+ '54.x': [620.71, 620.54],
+ '54.y': [65.38, 50.05],
+ '55.height': [14.4, 11.02],
+ '55.width': [14.05, 10.76],
+ '55.x': [620.27, 620.21],
+ '55.y': [51.7, 39.57],
+ '56.height': [11.96, 9.15],
+ '56.width': [10.9, 8.35],
+ '56.x': [622.49, 621.91],
+ '56.y': [63.37, 48.51],
+ '57.height': [13.86, 10.61],
+ '57.width': [6.99, 5.35],
+ '57.x': [627.87, 626.02],
+ '57.y': [64.61, 49.46],
+ '58.height': [6.33, 4.85],
+ '58.width': [1.77, 1.36],
+ '58.x': [619.96, 619.97],
+ '58.y': [66.89, 51.2],
+ '59.height': [7.95, 6.09],
+ '59.width': [3.99, 3.05],
+ '59.x': [616.99, 617.7],
+ '59.y': [66.79, 51.12],
+ '60.height': [7.7, 5.9],
+ '60.width': [5.6, 4.28],
+ '60.x': [612.03, 613.9],
+ '60.y': [67.5, 51.67],
+ '61.height': [6.55, 5.01],
+ '61.width': [6.69, 5.12],
+ '61.x': [606.07, 609.34],
+ '61.y': [64.95, 49.71],
+ '62.height': [1.96, 1.5],
+ '62.width': [2.74, 2.1],
+ '62.x': [604.68, 608.28],
+ '62.y': [64.9, 49.67],
+ '63.height': [5.22, 4],
+ '63.width': [3.6, 2.76],
+ '63.x': [609.41, 611.9],
+ '63.y': [69.96, 53.55],
+ '64.height': [4.06, 3.11],
+ '64.width': [2.78, 2.13],
+ '64.x': [607.77, 610.64],
+ '64.y': [68.09, 52.12],
+ '65.height': [6.8, 5.21],
+ '65.width': [7.03, 5.38],
+ '65.x': [614.96, 616.15],
+ '65.y': [61.83, 47.33],
+ '66.height': [11.06, 8.47],
+ '66.width': [11.97, 9.16],
+ '66.x': [633.4, 630.26],
+ '66.y': [66.73, 51.08],
+ '67.height': [10.96, 8.39],
+ '67.width': [6.83, 5.23],
+ '67.x': [630.29, 627.88],
+ '67.y': [75.97, 58.15],
+ '68.height': [7.89, 6.04],
+ '68.width': [5.23, 4],
+ '68.x': [628.17, 626.25],
+ '68.y': [77.72, 59.49],
+ '69.height': [1.59, 1.22],
+ '69.width': [1.84, 1.41],
+ '69.x': [628.64, 626.61],
+ '69.y': [77.77, 59.53],
+ '70.height': [11.99, 9.18],
+ '70.width': [10.9, 8.35],
+ '70.x': [639.95, 635.27],
+ '70.y': [90.92, 69.59],
+ '71.height': [9.34, 7.15],
+ '71.width': [2.69, 2.06],
+ '71.x': [649.94, 642.92],
+ '71.y': [92.16, 70.54],
+ '72.height': [19.94, 15.27],
+ '72.width': [9.69, 7.42],
+ '72.x': [647.59, 641.12],
+ '72.y': [93.5, 71.57],
+ '73.height': [1.92, 1.47],
+ '73.width': [1.22, 0.94],
+ '73.x': [647.03, 640.69],
+ '73.y': [112.17, 85.86],
+ '74.height': [16.69, 12.78],
+ '74.width': [11.53, 8.82],
+ '74.x': [630.59, 628.11],
+ '74.y': [86.18, 65.97],
+ '75.height': [2.69, 2.06],
+ '75.width': [1.6, 1.22],
+ '75.x': [646.94, 640.62],
+ '75.y': [83.57, 63.97],
+ '76.height': [4.34, 3.32],
+ '76.width': [1.11, 0.85],
+ '76.x': [649.6, 642.66],
+ '76.y': [40.25, 30.81],
+ '77.height': [1.76, 1.35],
+ '77.width': [1.09, 0.83],
+ '77.x': [650.07, 643.01],
+ '77.y': [38.7, 29.62],
+ '78.height': [16.46, 12.6],
+ '78.width': [7.34, 5.62],
+ '78.x': [658.75, 649.66],
+ '78.y': [95.64, 73.21],
+ '79.height': [1.35, 1.03],
+ '79.width': [0.5, 0.38],
+ '79.x': [650.12, 643.05],
+ '79.y': [41.09, 31.46],
+ '80.height': [0.92, 0.7],
+ '80.width': [2.76, 2.11],
+ '80.x': [604.55, 608.18],
+ '80.y': [63.36, 48.5],
+ '81.height': [7.97, 6.1],
+ '81.width': [3.42, 2.62],
+ '81.x': [626.41, 624.91],
+ '81.y': [35.68, 27.31],
+ '82.height': [21.08, 16.13],
+ '82.width': [18.33, 14.03],
+ '82.x': [612.37, 614.16],
+ '82.y': [35.94, 27.51],
+ '83.height': [4.81, 3.68],
+ '83.width': [3.3, 2.52],
+ '83.x': [650.41, 643.27],
+ '83.y': [40.13, 30.72],
+ '84.height': [4.24, 3.25],
+ '84.width': [4.16, 3.18],
+ '84.x': [665.87, 655.11],
+ '84.y': [48.62, 37.22],
+ '85.height': [1.85, 1.41],
+ '85.width': [0.81, 0.62],
+ '85.x': [664.92, 654.39],
+ '85.y': [48.55, 37.17],
+ '86.height': [1.78, 1.37],
+ '86.width': [1.71, 1.31],
+ '86.x': [660.62, 651.09],
+ '86.y': [43.94, 33.63],
+ '87.height': [9.41, 7.2],
+ '87.width': [9.05, 6.93],
+ '87.x': [653.16, 645.38],
+ '87.y': [35.64, 27.28],
+ '88.height': [11.73, 8.98],
+ '88.width': [6.51, 4.99],
+ '88.x': [667.03, 656],
+ '88.y': [48.22, 36.91],
+ '89.height': [2.42, 1.85],
+ '89.width': [0.71, 0.54],
+ '89.x': [772.12, 736.44],
+ '89.y': [98.83, 75.65],
+ '90.height': [5.05, 3.86],
+ '90.width': [4.91, 3.76],
+ '90.x': [713.82, 691.81],
+ '90.y': [62.5, 47.84],
+ '91.height': [18.15, 13.89],
+ '91.width': [8.27, 6.33],
+ '91.x': [708.21, 687.52],
+ '91.y': [55.37, 42.38],
+ '92.height': [10.45, 8],
+ '92.width': [8.09, 6.2],
+ '92.x': [710.18, 689.03],
+ '92.y': [52.9, 40.49],
+ '93.height': [22.29, 17.06],
+ '93.width': [8.67, 6.64],
+ '93.x': [702.87, 683.43],
+ '93.y': [45.94, 35.17],
+ '94.height': [18.05, 13.82],
+ '94.width': [8.99, 6.88],
+ '94.x': [711.43, 689.99],
+ '94.y': [51.84, 39.68],
+ '95.height': [5.73, 4.39],
+ '95.width': [4.26, 3.26],
+ '95.x': [722.34, 698.33],
+ '95.y': [29.6, 22.66],
+ '96.height': [4.69, 3.59],
+ '96.width': [4.07, 3.11],
+ '96.x': [725.28, 700.59],
+ '96.y': [34.29, 26.25],
+ '97.height': [10.56, 8.09],
+ '97.width': [26.47, 20.26],
+ '97.x': [687.61, 671.75],
+ '97.y': [20.51, 15.7],
+ '98.height': [32.93, 25.21],
+ '98.width': [24.7, 18.9],
+ '98.x': [680.75, 666.5],
+ '98.y': [37.75, 28.89],
+ '99.height': [6.9, 5.28],
+ '99.width': [5.22, 3.99],
+ '99.x': [698.01, 679.71],
+ '99.y': [48.16, 36.87],
+ '100.height': [1.85, 1.42],
+ '100.width': [2.98, 2.28],
+ '100.x': [698.06, 679.75],
+ '100.y': [45.99, 35.2],
+ '101.height': [4.7, 3.6],
+ '101.width': [7.73, 5.91],
+ '101.x': [689.95, 673.54],
+ '101.y': [43.52, 33.31],
+ '102.height': [15.51, 11.87],
+ '102.width': [12.86, 9.85],
+ '102.x': [672.93, 660.51],
+ '102.y': [35.92, 27.5],
+ '103.height': [10.36, 7.93],
+ '103.width': [11.29, 8.64],
+ '103.x': [671.66, 659.54],
+ '103.y': [34.43, 26.36],
+ '104.height': [4.61, 3.53],
+ '104.width': [6.88, 5.26],
+ '104.x': [675.81, 662.72],
+ '104.y': [31.75, 24.3],
+ '105.height': [4.3, 3.29],
+ '105.width': [7.85, 6.01],
+ '105.x': [677.22, 663.8],
+ '105.y': [29.27, 22.4],
+ '106.height': [8.15, 6.24],
+ '106.width': [12.91, 9.88],
+ '106.x': [662.73, 652.7],
+ '106.y': [29.85, 22.85],
+ '107.height': [16.69, 12.77],
+ '107.width': [19.34, 14.8],
+ '107.x': [656.44, 647.89],
+ '107.y': [33.1, 25.33],
+ '108.height': [5.53, 4.23],
+ '108.width': [5.09, 3.89],
+ '108.x': [650.36, 643.24],
+ '108.y': [35.82, 27.41],
+ '109.height': [2.71, 2.07],
+ '109.width': [2.95, 2.25],
+ '109.x': [655.63, 647.27],
+ '109.y': [31.44, 24.07],
+ '110.height': [10.81, 8.28],
+ '110.width': [7.91, 6.06],
+ '110.x': [627.8, 625.97],
+ '110.y': [6.62, 5.07],
+ '111.height': [4.49, 3.44],
+ '111.width': [6.94, 5.32],
+ '111.x': [637.4, 633.32],
+ '111.y': [16.71, 12.79],
+ '112.height': [6.96, 5.33],
+ '112.width': [13.87, 10.61],
+ '112.x': [637.13, 633.11],
+ '112.y': [20.23, 15.49],
+ '113.height': [5.53, 4.23],
+ '113.width': [7.82, 5.99],
+ '113.x': [630.48, 628.02],
+ '113.y': [17.9, 13.7],
+ '114.height': [2.61, 2],
+ '114.width': [5.77, 4.42],
+ '114.x': [627.43, 625.69],
+ '114.y': [23.42, 17.93],
+ '115.height': [2.89, 2.21],
+ '115.width': [4.97, 3.81],
+ '115.x': [632.72, 629.74],
+ '115.y': [23.83, 18.24],
+ '116.height': [3.01, 2.3],
+ '116.width': [2.97, 2.27],
+ '116.x': [640.69, 635.83],
+ '116.y': [23.99, 18.36],
+ '117.height': [4.63, 3.55],
+ '117.width': [7.61, 5.83],
+ '117.x': [635.96, 632.22],
+ '117.y': [24.23, 18.55],
+ '118.height': [2.24, 1.71],
+ '118.width': [4.18, 3.2],
+ '118.x': [635.31, 631.72],
+ '118.y': [16.53, 12.65],
+ '119.height': [2.08, 1.59],
+ '119.width': [5.23, 4],
+ '119.x': [635.22, 631.65],
+ '119.y': [15.13, 11.58],
+ '120.height': [1.82, 1.4],
+ '120.width': [3.36, 2.57],
+ '120.x': [636.46, 632.6],
+ '120.y': [13.74, 10.51],
+ '121.height': [7.37, 5.64],
+ '121.width': [6.88, 5.27],
+ '121.x': [624.52, 623.46],
+ '121.y': [17.78, 13.61],
+ '122.height': [3.17, 2.42],
+ '122.width': [4.97, 3.81],
+ '122.x': [637.97, 633.75],
+ '122.y': [28.29, 21.65],
+ '123.height': [7.57, 5.79],
+ '123.width': [5.62, 4.3],
+ '123.x': [636.63, 632.73],
+ '123.y': [30.83, 23.59],
+ '124.height': [6.89, 5.27],
+ '124.width': [16.24, 12.43],
+ '124.x': [641.25, 636.26],
+ '124.y': [30.49, 23.34],
+ '125.height': [3.28, 2.51],
+ '125.width': [1.56, 1.19],
+ '125.x': [635.67, 631.99],
+ '125.y': [29.91, 22.9],
+ '126.height': [4.17, 3.19],
+ '126.width': [4.56, 3.49],
+ '126.x': [630.85, 628.3],
+ '126.y': [25.96, 19.87],
+ '127.height': [2.08, 1.59],
+ '127.width': [3.45, 2.64],
+ '127.x': [624.75, 623.63],
+ '127.y': [24.62, 18.84],
+ '128.height': [0.67, 0.51],
+ '128.width': [0.42, 0.32],
+ '128.x': [624.36, 623.34],
+ '128.y': [22.35, 17.11],
+ '129.height': [1.88, 1.44],
+ '129.width': [2.78, 2.13],
+ '129.x': [621.91, 621.46],
+ '129.y': [21.05, 16.11],
+ '130.height': [2.56, 1.96],
+ '130.width': [2.78, 2.13],
+ '130.x': [622.51, 621.92],
+ '130.y': [19.13, 14.64],
+ '131.height': [5.9, 4.52],
+ '131.width': [2.69, 2.06],
+ '131.x': [612.1, 613.95],
+ '131.y': [30.34, 23.23],
+ '132.height': [8.44, 6.46],
+ '132.width': [10.05, 7.7],
+ '132.x': [612.42, 614.2],
+ '132.y': [28.8, 22.04],
+ '133.height': [3.22, 2.47],
+ '133.width': [3.03, 2.32],
+ '133.x': [612.47, 614.24],
+ '133.y': [17.64, 13.5],
+ '134.height': [2.77, 2.12],
+ '134.width': [1.84, 1.41],
+ '134.x': [767.86, 733.18],
+ '134.y': [105.52, 80.77],
+ '135.height': [5.26, 4.02],
+ '135.width': [4.69, 3.59],
+ '135.x': [764.86, 730.88],
+ '135.y': [88.88, 68.03],
+ '136.height': [13.1, 10.03],
+ '136.width': [17.51, 13.41],
+ '136.x': [751.5, 720.65],
+ '136.y': [122.34, 93.65],
+ '137.height': [38.41, 29.4],
+ '137.width': [37.54, 28.73],
+ '137.x': [717.9, 694.93],
+ '137.y': [93.94, 71.9],
+ '138.height': [4.8, 3.67],
+ '138.width': [1.99, 1.52],
+ '138.x': [693.66, 676.38],
+ '138.y': [68.37, 52.33],
+ '139.height': [38.88, 29.76],
+ '139.width': [45.73, 35.01],
+ '139.x': [680.81, 666.54],
+ '139.y': [19.18, 14.68],
+ '140.height': [3.97, 3.04],
+ '140.width': [1.24, 0.95],
+ '140.x': [727.08, 701.96],
+ '140.y': [49.52, 37.91],
+ '141.height': [11.15, 8.54],
+ '141.width': [9.83, 7.52],
+ '141.x': [625.37, 624.11],
+ '141.y': [25.34, 19.4],
+ '142.height': [2.61, 2],
+ '142.width': [3.41, 2.61],
+ '142.x': [625.85, 624.48],
+ '142.y': [15.34, 11.74],
+ '143.height': [7.95, 6.09],
+ '143.width': [6.86, 5.25],
+ '143.x': [614.4, 615.71],
+ '143.y': [14.56, 11.15],
+ '144.height': [2.24, 1.71],
+ '144.width': [7.03, 5.38],
+ '144.x': [603.93, 607.7],
+ '144.y': [8.38, 6.41],
+ '145.height': [3.88, 2.97],
+ '145.width': [4.69, 3.59],
+ '145.x': [656.73, 648.11],
+ '145.y': [30.79, 23.57],
+ '146.height': [2.64, 2.02],
+ '146.width': [5.95, 4.55],
+ '146.x': [652.16, 644.61],
+ '146.y': [29, 22.2],
+ '147.height': [15.97, 12.22],
+ '147.width': [8.81, 6.74],
+ '147.x': [728.28, 702.88],
+ '147.y': [57.69, 44.16],
+ '148.height': [7.69, 5.89],
+ '148.width': [17.78, 13.61],
+ '148.x': [712.67, 690.94],
+ '148.y': [71.97, 55.09],
+ '149.height': [1.8, 1.38],
+ '149.width': [1.08, 0.83],
+ '149.x': [725.91, 701.07],
+ '149.y': [73.82, 56.5],
+ '150.height': [1.43, 1.09],
+ '150.width': [2.22, 1.7],
+ '150.x': [630.82, 628.28],
+ '150.y': [25.6, 19.6],
+ '151.height': [7.59, 5.81],
+ '151.width': [8.28, 6.34],
+ '151.x': [633.14, 630.05],
+ '151.y': [5.95, 4.55],
+ '152.height': [1.8, 1.38],
+ '152.width': [4.32, 3.31],
+ '152.x': [633.09, 630.02],
+ '152.y': [22.89, 17.52],
+ '153.height': [2.5, 1.92],
+ '153.width': [5.15, 3.94],
+ '153.x': [629.36, 627.16],
+ '153.y': [21.39, 16.38],
+ '154.height': [6.8, 5.21],
+ '154.width': [6.35, 4.86],
+ '154.x': [653.2, 645.41],
+ '154.y': [58.31, 44.63],
+ '155.height': [15.89, 12.16],
+ '155.width': [8.12, 6.21],
+ '155.x': [730.65, 704.7],
+ '155.y': [26.93, 20.61],
+ '156.height': [9.81, 7.51],
+ '156.width': [7.91, 6.05],
+ '156.x': [563.86, 577.03],
+ '156.y': [104.59, 80.06],
+ '157.height': [7.86, 6.02],
+ '157.width': [9.43, 7.22],
+ '157.x': [658.91, 649.78],
+ '157.y': [57.09, 43.7],
+ '158.height': [18.8, 14.39],
+ '158.width': [19.55, 14.96],
+ '158.x': [650.34, 643.23],
+ '158.y': [41.52, 31.78],
+ '159.height': [11.53, 8.82],
+ '159.width': [206.24, 157.86],
+ '159.x': [519.76, 543.27],
+ '159.y': [150.47, 115.18],
+ '160.height': [0.75, 0.57],
+ '160.width': [1.48, 1.13],
+ '160.x': [647.72, 641.22],
+ '160.y': [37.55, 28.74],
+ '161.height': [0.68, 0.52],
+ '161.width': [1.5, 1.15],
+ '161.x': [647.32, 640.91],
+ '161.y': [38.11, 29.17],
+ '162.height': [16.71, 12.79],
+ '162.width': [14.32, 10.96],
+ '162.x': [604.72, 608.3],
+ '162.y': [37.45, 28.67],
+ '163.height': [11.28, 8.63],
+ '163.width': [11.6, 8.88],
+ '163.x': [641.44, 636.41],
+ '163.y': [42.18, 32.29],
+ '164.height': [15.98, 12.23],
+ '164.width': [14.32, 10.96],
+ '164.x': [628.22, 626.3],
+ '164.y': [40.41, 30.93],
+ '165.height': [14.33, 10.97],
+ '165.width': [13.71, 10.49],
+ '165.x': [650.47, 643.32],
+ '165.y': [62.02, 47.48],
+ '166.height': [2.19, 1.68],
+ '166.width': [1.47, 1.13],
+ '166.x': [658.31, 649.33],
+ '166.y': [64.81, 49.61],
+ '167.height': [4.3, 3.29],
+ '167.width': [5.95, 4.55],
+ '167.x': [659.21, 650.01],
+ '167.y': [66.34, 50.78],
+ '168.height': [7.12, 5.45],
+ '168.width': [5.06, 3.88],
+ '168.x': [647.47, 641.03],
+ '168.y': [75.32, 57.65],
+ '169.height': [2.23, 1.71],
+ '169.width': [1.67, 1.28],
+ '169.x': [646.94, 640.62],
+ '169.y': [82.05, 62.81],
+ '170.height': [2.7, 2.06],
+ '170.width': [3.16, 2.42],
+ '170.x': [632.55, 629.61],
+ '170.y': [27.26, 20.86],
+ '171.height': [1.58, 1.21],
+ '171.width': [2.05, 1.57],
+ '171.x': [636.68, 632.77],
+ '171.y': [30.3, 23.19],
+ '172.height': [4.09, 3.13],
+ '172.width': [3.64, 2.78],
+ '172.x': [634.89, 631.4],
+ '172.y': [26.3, 20.13],
+ '173.height': [1.74, 1.33],
+ '173.width': [1.48, 1.14],
+ '173.x': [634.94, 631.44],
+ '173.y': [29.03, 22.22],
+ '174.height': [1.51, 1.15],
+ '174.width': [1.37, 1.05],
+ '174.x': [636.25, 632.44],
+ '174.y': [29.3, 22.42],
+ '175.height': [1.1, 0.85],
+ '175.width': [1.04, 0.79],
+ '175.x': [562.89, 576.28],
+ '175.y': [67.05, 51.32],
+ '176.height': [10.87, 8.32],
+ '176.width': [10.64, 8.15],
+ '176.x': [642.07, 636.89],
+ '176.y': [65.37, 50.03],
+ },
+ legend: {
+ 'continuousLegend.bounds.height': [61, 99],
+ 'continuousLegend.bar.y': [107.2, 122.2],
+ },
+ },
+ style: {
+ 'legends.continuousLegend.title': ['GDP per capita (US$) ($)', 'GDP per capita (US$)'],
+ },
+ },
+ '../testing/fixtures-maps.stories.tsx.worldEqualEarthSpec @390': {
+ reason:
+ 'Approved B3 static map captions and neutral-class context reserve measured key space; occupied-bounds size-key placement has no legacy layout opt-out.',
+ changes: {
+ dims: {
+ 'mapSize.height': [292.4, 269.4],
+ },
+ area: {
+ 'rect.height': [292.4, 269.4],
+ 'rect.y': [64, 87],
+ },
+ marks: {
+ '0.y': [168.24, 156.74],
+ '1.y': [147.13, 135.63],
+ '2.y': [107.56, 96.06],
+ '3.y': [54.97, 43.47],
+ '4.y': [60.74, 49.24],
+ '5.y': [74.49, 62.99],
+ '6.y': [85.19, 73.69],
+ '7.y': [149.31, 137.81],
+ '8.y': [138.06, 126.56],
+ '9.y': [176.2, 164.7],
+ '10.y': [170.39, 158.89],
+ '11.y': [138.37, 126.87],
+ '12.y': [128.88, 117.38],
+ '13.y': [138.02, 126.52],
+ '14.y': [115.14, 103.64],
+ '15.y': [113.24, 101.74],
+ '16.y': [117.98, 106.48],
+ '17.y': [118.02, 106.52],
+ '18.y': [55.59, 44.09],
+ '19.y': [108.38, 96.88],
+ '20.y': [212.57, 201.07],
+ '21.y': [55.8, 44.3],
+ '22.y': [54.86, 43.36],
+ '23.y': [209.84, 198.34],
+ '24.y': [157.44, 145.94],
+ '25.y': [176.55, 165.05],
+ '26.y': [185.31, 173.81],
+ '27.y': [100.96, 89.46],
+ '28.y': [187.23, 175.73],
+ '29.y': [138.39, 126.89],
+ '30.y': [159.52, 148.02],
+ '31.y': [145.86, 134.36],
+ '32.y': [128.31, 116.81],
+ '33.y': [132.26, 120.76],
+ '34.y': [130.01, 118.51],
+ '35.y': [124.72, 113.22],
+ '36.y': [123.35, 111.85],
+ '37.y': [125.54, 114.04],
+ '38.y': [120.85, 109.35],
+ '39.y': [119.92, 108.42],
+ '40.y': [128.69, 117.19],
+ '41.y': [134, 122.5],
+ '42.y': [137.29, 125.79],
+ '43.y': [78.95, 67.45],
+ '44.y': [143.83, 132.33],
+ '45.y': [119.89, 108.39],
+ '46.y': [119.89, 108.39],
+ '47.y': [113.54, 102.04],
+ '48.y': [167.53, 156.03],
+ '49.y': [170.5, 159],
+ '50.y': [169.51, 158.01],
+ '51.y': [122.54, 111.04],
+ '52.y': [111.14, 99.64],
+ '53.y': [107.91, 96.41],
+ '54.y': [128.59, 117.09],
+ '55.y': [113.15, 101.65],
+ '56.y': [126.32, 114.82],
+ '57.y': [127.72, 116.22],
+ '58.y': [130.29, 118.79],
+ '59.y': [130.17, 118.67],
+ '60.y': [130.98, 119.48],
+ '61.y': [128.1, 116.6],
+ '62.y': [128.04, 116.54],
+ '63.y': [133.76, 122.26],
+ '64.y': [131.64, 120.14],
+ '65.y': [124.58, 113.08],
+ '66.y': [130.11, 118.61],
+ '67.y': [140.53, 129.03],
+ '68.y': [142.5, 131],
+ '69.y': [142.56, 131.06],
+ '70.y': [157.39, 145.89],
+ '71.y': [158.78, 147.28],
+ '72.y': [160.3, 148.8],
+ '73.y': [181.35, 169.85],
+ '74.y': [152.04, 140.54],
+ '75.y': [149.1, 137.6],
+ '76.y': [100.24, 88.74],
+ '77.y': [98.5, 87],
+ '78.y': [162.71, 151.21],
+ '79.y': [101.2, 89.7],
+ '80.y': [126.3, 114.8],
+ '81.y': [95.1, 83.6],
+ '82.y': [95.38, 83.88],
+ '83.y': [100.11, 88.61],
+ '84.y': [109.69, 98.19],
+ '85.y': [109.61, 98.11],
+ '86.y': [104.41, 92.91],
+ '87.y': [95.05, 83.55],
+ '88.y': [109.24, 97.74],
+ '89.y': [166.31, 154.81],
+ '90.y': [125.34, 113.84],
+ '91.y': [117.29, 105.79],
+ '92.y': [114.52, 103.02],
+ '93.y': [106.67, 95.17],
+ '94.y': [113.31, 101.81],
+ '95.y': [88.24, 76.74],
+ '96.y': [93.53, 82.03],
+ '97.y': [77.98, 66.48],
+ '98.y': [97.42, 85.92],
+ '99.y': [109.17, 97.67],
+ '100.y': [106.72, 95.22],
+ '101.y': [103.93, 92.43],
+ '102.y': [95.37, 83.87],
+ '103.y': [93.69, 82.19],
+ '104.y': [90.66, 79.16],
+ '105.y': [87.86, 76.36],
+ '106.y': [88.51, 77.01],
+ '107.y': [92.18, 80.68],
+ '108.y': [95.25, 83.75],
+ '109.y': [90.31, 78.81],
+ '110.y': [62.32, 50.82],
+ '111.y': [73.7, 62.2],
+ '112.y': [77.67, 66.17],
+ '113.y': [75.04, 63.54],
+ '114.y': [81.27, 69.77],
+ '115.y': [81.73, 70.23],
+ '116.y': [81.9, 70.4],
+ '117.y': [82.18, 70.68],
+ '118.y': [73.5, 62],
+ '119.y': [71.92, 60.42],
+ '120.y': [70.35, 58.85],
+ '121.y': [74.9, 63.4],
+ '122.y': [86.76, 75.26],
+ '123.y': [89.62, 78.12],
+ '124.y': [89.24, 77.74],
+ '125.y': [88.59, 77.09],
+ '126.y': [84.13, 72.63],
+ '127.y': [82.62, 71.12],
+ '128.y': [80.06, 68.56],
+ '129.y': [78.6, 67.1],
+ '130.y': [76.43, 64.93],
+ '131.y': [89.07, 77.57],
+ '132.y': [87.33, 75.83],
+ '133.y': [74.75, 63.25],
+ '134.y': [173.85, 162.35],
+ '135.y': [155.08, 143.58],
+ '136.y': [192.82, 181.32],
+ '137.y': [160.79, 149.29],
+ '138.y': [131.96, 120.46],
+ '139.y': [76.49, 64.99],
+ '140.y': [110.7, 99.2],
+ '141.y': [83.43, 71.93],
+ '142.y': [72.15, 60.65],
+ '143.y': [71.28, 59.78],
+ '144.y': [64.3, 52.8],
+ '145.y': [89.58, 78.08],
+ '146.y': [87.56, 76.06],
+ '147.y': [119.91, 108.41],
+ '148.y': [136.02, 124.52],
+ '149.y': [138.1, 126.6],
+ '150.y': [83.73, 72.23],
+ '151.y': [61.56, 50.06],
+ '152.y': [80.67, 69.17],
+ '153.y': [78.98, 67.48],
+ '154.y': [120.61, 109.11],
+ '155.y': [85.23, 73.73],
+ '156.y': [172.8, 161.3],
+ '157.y': [119.23, 107.73],
+ '158.y': [101.68, 90.18],
+ '159.y': [224.55, 213.05],
+ '160.y': [97.2, 85.7],
+ '161.y': [97.83, 86.33],
+ '162.y': [97.09, 85.59],
+ '163.y': [102.42, 90.92],
+ '164.y': [100.42, 88.92],
+ '165.y': [124.8, 113.3],
+ '166.y': [127.94, 116.44],
+ '167.y': [129.67, 118.17],
+ '168.y': [139.79, 128.29],
+ '169.y': [147.39, 135.89],
+ '170.y': [85.59, 74.09],
+ '171.y': [89.03, 77.53],
+ '172.y': [84.51, 73.01],
+ '173.y': [87.6, 76.1],
+ '174.y': [87.89, 76.39],
+ '175.y': [130.47, 118.97],
+ '176.y': [128.57, 117.07],
+ },
+ legend: {
+ 'continuousLegend.bounds.height': [46, 69],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-maps.stories.tsx.worldEqualEarthSpec @1280': {
+ reason:
+ 'Approved B3 static map captions and neutral-class context reserve measured key space; occupied-bounds size-key placement has no legacy layout opt-out.',
+ changes: {
+ dims: {
+ 'mapSize.height': [262.4, 239.4],
+ },
+ area: {
+ 'rect.height': [262.4, 239.4],
+ 'rect.y': [74, 97],
+ },
+ marks: {
+ '0.height': [4.48, 4.09],
+ '0.width': [531.44, 484.86],
+ '0.x': [354.28, 377.57],
+ '0.y': [162.85, 148.58],
+ '1.height': [21.74, 19.84],
+ '1.width': [16.11, 14.7],
+ '1.x': [664.07, 660.21],
+ '1.y': [132.53, 120.91],
+ '2.height': [12.84, 11.71],
+ '2.width': [12.53, 11.43],
+ '2.x': [595.15, 597.33],
+ '2.y': [75.71, 69.07],
+ '3.height': [50.03, 45.65],
+ '3.width': [104.03, 94.92],
+ '3.x': [459.56, 473.63],
+ '3.y': [0.16, 0.14],
+ '4.height': [84.18, 76.8],
+ '4.width': [145.86, 133.08],
+ '4.x': [388.06, 408.39],
+ '4.y': [8.45, 7.71],
+ '5.height': [23.74, 21.66],
+ '5.width': [50.78, 46.33],
+ '5.x': [678.32, 673.21],
+ '5.y': [28.21, 25.74],
+ '6.height': [14.6, 13.32],
+ '6.width': [24.76, 22.59],
+ '6.x': [692.11, 685.79],
+ '6.y': [43.56, 39.74],
+ '7.height': [16.46, 15.02],
+ '7.width': [23.1, 21.07],
+ '7.x': [830.89, 812.4],
+ '7.y': [135.67, 123.78],
+ '8.height': [32.03, 29.23],
+ '8.width': [68.97, 62.92],
+ '8.x': [763.05, 750.51],
+ '8.y': [119.51, 109.03],
+ '9.height': [58.52, 53.39],
+ '9.width': [22.3, 20.35],
+ '9.x': [521.2, 529.86],
+ '9.y': [174.29, 159.01],
+ '10.height': [67.39, 61.48],
+ '10.width': [23.62, 21.55],
+ '10.x': [516.72, 525.77],
+ '10.y': [165.94, 151.39],
+ '11.height': [37.39, 34.11],
+ '11.width': [28.6, 26.09],
+ '11.x': [638.28, 636.68],
+ '11.y': [119.96, 109.44],
+ '12.height': [27.69, 25.26],
+ '12.width': [14.55, 13.27],
+ '12.x': [681.62, 676.22],
+ '12.y': [106.33, 97.01],
+ '13.height': [20.62, 18.82],
+ '13.width': [11.91, 10.87],
+ '13.x': [670.99, 666.52],
+ '13.y': [119.45, 108.98],
+ '14.height': [27.36, 24.96],
+ '14.width': [23.81, 21.72],
+ '14.x': [652.62, 649.76],
+ '14.y': [86.59, 79],
+ '15.height': [31.73, 28.95],
+ '15.width': [15.24, 13.9],
+ '15.x': [640.07, 638.31],
+ '15.y': [83.85, 76.5],
+ '16.height': [3.71, 3.38],
+ '16.width': [4.44, 4.05],
+ '16.x': [510.72, 520.3],
+ '16.y': [90.67, 82.72],
+ '17.height': [4.5, 4.11],
+ '17.width': [5.32, 4.85],
+ '17.x': [514.48, 523.73],
+ '17.y': [90.73, 82.78],
+ '18.height': [50.05, 45.66],
+ '18.width': [392.28, 357.89],
+ '18.x': [426.17, 443.16],
+ '18.y': [1.05, 0.96],
+ '19.height': [6.39, 5.83],
+ '19.width': [2.88, 2.63],
+ '19.x': [507.18, 517.07],
+ '19.y': [76.88, 70.14],
+ '20.height': [1.85, 1.69],
+ '20.width': [4.04, 3.68],
+ '20.x': [545.22, 551.77],
+ '20.y': [226.52, 206.67],
+ '21.height': [23, 20.98],
+ '21.width': [26.36, 24.05],
+ '21.x': [625.55, 625.06],
+ '21.y': [1.36, 1.24],
+ '22.height': [21.67, 19.77],
+ '22.width': [57.91, 52.83],
+ '22.x': [550.87, 556.93],
+ '23.height': [1.83, 1.67],
+ '23.width': [2.72, 2.48],
+ '23.x': [705.46, 697.97],
+ '23.y': [222.61, 203.1],
+ '24.height': [2.26, 2.06],
+ '24.width': [3.66, 3.34],
+ '24.x': [806.92, 790.54],
+ '24.y': [147.34, 134.42],
+ '25.height': [24.05, 21.94],
+ '25.width': [23.7, 21.62],
+ '25.x': [643.15, 641.12],
+ '25.y': [174.79, 159.47],
+ '26.height': [3.75, 3.43],
+ '26.width': [3.39, 3.09],
+ '26.x': [658.01, 654.68],
+ '26.y': [187.38, 170.95],
+ '27.height': [35.09, 32.01],
+ '27.width': [37.21, 33.95],
+ '27.x': [456.49, 470.82],
+ '27.y': [66.22, 60.41],
+ '28.height': [8.95, 8.17],
+ '28.width': [7.32, 6.68],
+ '28.x': [538.94, 546.05],
+ '28.y': [190.13, 173.46],
+ '29.height': [76.93, 70.19],
+ '29.width': [58.8, 53.65],
+ '29.x': [509.16, 518.87],
+ '29.y': [119.98, 109.47],
+ '30.height': [25.97, 23.69],
+ '30.width': [19.26, 17.57],
+ '30.x': [516.31, 525.4],
+ '30.y': [150.34, 137.16],
+ '31.height': [36.74, 33.52],
+ '31.width': [20.58, 18.77],
+ '31.x': [497.73, 508.45],
+ '31.y': [130.72, 119.26],
+ '32.height': [33.81, 30.85],
+ '32.width': [18.21, 16.61],
+ '32.x': [501.19, 511.61],
+ '32.y': [105.5, 96.25],
+ '33.height': [4.82, 4.39],
+ '33.width': [8.52, 7.78],
+ '33.x': [495.8, 506.69],
+ '33.y': [111.17, 101.43],
+ '34.height': [6.02, 5.49],
+ '34.width': [4.82, 4.4],
+ '34.x': [491.83, 503.06],
+ '34.y': [107.94, 98.48],
+ '35.height': [8.58, 7.83],
+ '35.width': [7.25, 6.61],
+ '35.x': [489.71, 501.13],
+ '35.y': [100.35, 91.55],
+ '36.height': [6.02, 5.49],
+ '36.width': [9.35, 8.53],
+ '36.x': [487.61, 499.21],
+ '36.y': [98.39, 89.76],
+ '37.height': [2.55, 2.32],
+ '37.width': [3.53, 3.22],
+ '37.x': [486.32, 498.03],
+ '37.y': [101.53, 92.63],
+ '38.height': [8.11, 7.4],
+ '38.width': [6.28, 5.73],
+ '38.x': [483.38, 495.36],
+ '38.y': [94.79, 86.48],
+ '39.height': [5.17, 4.72],
+ '39.width': [2.46, 2.25],
+ '39.x': [488.22, 499.77],
+ '39.y': [93.45, 85.26],
+ '40.height': [23.08, 21.06],
+ '40.width': [20.17, 18.4],
+ '40.x': [510.39, 520],
+ '40.y': [106.05, 96.76],
+ '41.height': [14.36, 13.1],
+ '41.width': [7.11, 6.49],
+ '41.x': [527.85, 535.93],
+ '41.y': [113.68, 103.71],
+ '42.height': [8.52, 7.77],
+ '42.width': [6.24, 5.69],
+ '42.x': [532.78, 540.43],
+ '42.y': [118.4, 108.02],
+ '43.height': [91.84, 83.79],
+ '43.width': [94.56, 86.28],
+ '43.x': [538.01, 545.19],
+ '43.y': [34.61, 31.57],
+ '44.height': [12.84, 11.72],
+ '44.width': [8.58, 7.83],
+ '44.x': [498.24, 508.91],
+ '44.y': [127.8, 116.6],
+ '45.height': [1.13, 1.03],
+ '45.width': [2.42, 2.2],
+ '45.x': [521.29, 529.94],
+ '45.y': [93.41, 85.22],
+ '46.height': [1.62, 1.48],
+ '46.width': [3.04, 2.77],
+ '46.x': [504.98, 515.07],
+ '46.y': [93.4, 85.22],
+ '47.height': [6.5, 5.93],
+ '47.width': [15.13, 13.81],
+ '47.x': [496.6, 507.41],
+ '47.y': [84.28, 76.9],
+ '48.height': [13.31, 12.14],
+ '48.width': [11.28, 10.29],
+ '48.x': [657.14, 653.88],
+ '48.y': [161.83, 147.65],
+ '49.height': [17.82, 16.26],
+ '49.width': [14.11, 12.87],
+ '49.x': [648.61, 646.1],
+ '49.y': [166.1, 151.54],
+ '50.height': [23.45, 21.4],
+ '50.width': [19.61, 17.89],
+ '50.x': [637.27, 635.76],
+ '50.y': [164.68, 150.24],
+ '51.height': [8.5, 7.75],
+ '51.width': [9.05, 8.25],
+ '51.x': [593.9, 596.19],
+ '51.y': [97.21, 88.69],
+ '52.height': [29.36, 26.79],
+ '52.width': [24.32, 22.19],
+ '52.x': [601.98, 603.56],
+ '52.y': [80.84, 73.75],
+ '53.height': [24.94, 22.76],
+ '53.width': [17.78, 16.22],
+ '53.x': [595.15, 597.33],
+ '53.y': [76.2, 69.52],
+ '54.height': [12.26, 11.19],
+ '54.width': [4.51, 4.12],
+ '54.x': [621.15, 621.05],
+ '54.y': [105.9, 96.62],
+ '55.height': [23.32, 21.28],
+ '55.width': [22.77, 20.77],
+ '55.x': [620.44, 620.4],
+ '55.y': [83.74, 76.4],
+ '56.height': [19.37, 17.67],
+ '56.width': [17.66, 16.11],
+ '56.x': [624.04, 623.68],
+ '56.y': [102.64, 93.65],
+ '57.height': [22.45, 20.48],
+ '57.width': [11.33, 10.34],
+ '57.x': [632.75, 631.63],
+ '57.y': [104.66, 95.48],
+ '58.height': [10.25, 9.36],
+ '58.width': [2.87, 2.62],
+ '58.y': [108.34, 98.85],
+ '59.height': [12.88, 11.75],
+ '59.width': [6.46, 5.89],
+ '59.x': [615.13, 615.56],
+ '59.y': [108.18, 98.7],
+ '60.height': [12.48, 11.39],
+ '60.width': [9.07, 8.27],
+ '60.x': [607.1, 608.23],
+ '60.y': [109.33, 99.75],
+ '61.height': [10.61, 9.68],
+ '61.width': [10.84, 9.89],
+ '61.x': [597.44, 599.42],
+ '61.y': [105.2, 95.98],
+ '62.height': [3.18, 2.9],
+ '62.width': [4.44, 4.05],
+ '62.x': [595.19, 597.37],
+ '62.y': [105.12, 95.9],
+ '63.height': [8.45, 7.71],
+ '63.width': [5.83, 5.32],
+ '63.x': [602.85, 604.35],
+ '63.y': [113.33, 103.39],
+ '64.height': [6.57, 6],
+ '64.width': [4.5, 4.11],
+ '64.x': [600.19, 601.92],
+ '64.y': [110.29, 100.63],
+ '65.height': [11.02, 10.05],
+ '65.width': [11.39, 10.39],
+ '65.x': [611.84, 612.56],
+ '65.y': [100.15, 91.37],
+ '66.height': [17.91, 16.34],
+ '66.width': [19.39, 17.69],
+ '66.x': [641.71, 639.8],
+ '66.y': [108.09, 98.62],
+ '67.height': [17.76, 16.2],
+ '67.width': [11.06, 10.09],
+ '67.x': [636.67, 635.21],
+ '67.y': [123.05, 112.27],
+ '68.height': [12.77, 11.65],
+ '68.width': [8.47, 7.73],
+ '68.x': [633.23, 632.07],
+ '68.y': [125.89, 114.85],
+ '69.height': [2.58, 2.36],
+ '69.width': [2.98, 2.72],
+ '69.x': [634, 632.77],
+ '69.y': [125.97, 114.93],
+ '70.height': [19.42, 17.72],
+ '70.width': [17.66, 16.11],
+ '70.x': [652.31, 649.48],
+ '70.y': [147.27, 134.36],
+ '71.height': [15.13, 13.8],
+ '71.width': [4.35, 3.97],
+ '71.x': [668.5, 664.25],
+ '71.y': [149.27, 136.19],
+ '72.height': [32.3, 29.47],
+ '72.width': [15.7, 14.32],
+ '72.x': [664.68, 660.77],
+ '72.y': [151.45, 138.18],
+ '73.height': [3.1, 2.83],
+ '73.width': [1.98, 1.81],
+ '73.x': [663.78, 659.95],
+ '73.y': [181.69, 165.76],
+ '74.height': [27.04, 24.67],
+ '74.width': [18.67, 17.04],
+ '74.x': [637.16, 635.66],
+ '74.y': [139.59, 127.36],
+ '75.height': [4.36, 3.97],
+ '75.width': [2.59, 2.36],
+ '75.x': [663.64, 659.81],
+ '75.y': [135.36, 123.5],
+ '76.height': [7.03, 6.41],
+ '76.width': [1.79, 1.64],
+ '76.x': [667.95, 663.74],
+ '76.y': [65.19, 59.48],
+ '77.height': [2.85, 2.6],
+ '77.width': [1.77, 1.61],
+ '77.x': [668.7, 664.43],
+ '77.y': [62.68, 57.19],
+ '78.height': [26.66, 24.33],
+ '78.width': [11.9, 10.85],
+ '78.x': [682.77, 677.26],
+ '78.y': [154.91, 141.33],
+ '79.height': [2.19, 2],
+ '79.width': [0.81, 0.74],
+ '79.x': [668.79, 664.51],
+ '79.y': [66.56, 60.73],
+ '80.height': [1.49, 1.36],
+ '80.width': [4.47, 4.08],
+ '80.x': [594.98, 597.17],
+ '80.y': [102.62, 93.63],
+ '81.height': [12.91, 11.78],
+ '81.width': [5.55, 5.06],
+ '81.x': [630.38, 629.47],
+ '81.y': [57.8, 52.73],
+ '82.height': [34.14, 31.15],
+ '82.width': [29.7, 27.09],
+ '82.x': [607.64, 608.72],
+ '82.y': [58.21, 53.11],
+ '83.height': [7.79, 7.11],
+ '83.width': [5.34, 4.87],
+ '83.x': [669.25, 664.93],
+ '83.y': [65, 59.3],
+ '84.height': [6.87, 6.27],
+ '84.width': [6.74, 6.15],
+ '84.x': [694.3, 687.79],
+ '84.y': [78.76, 71.86],
+ '85.height': [2.99, 2.73],
+ '85.width': [1.31, 1.2],
+ '85.x': [692.77, 686.39],
+ '85.y': [78.65, 71.75],
+ '86.height': [2.89, 2.64],
+ '86.width': [2.77, 2.53],
+ '86.x': [685.79, 680.03],
+ '86.y': [71.17, 64.93],
+ '87.height': [15.24, 13.91],
+ '87.width': [14.66, 13.37],
+ '87.x': [673.7, 669],
+ '87.y': [57.73, 52.67],
+ '88.height': [19, 17.33],
+ '88.width': [10.55, 9.63],
+ '88.x': [696.18, 689.51],
+ '88.y': [78.11, 71.26],
+ '89.height': [3.92, 3.57],
+ '89.width': [1.14, 1.04],
+ '89.x': [866.4, 844.8],
+ '89.y': [160.08, 146.05],
+ '90.height': [8.17, 7.46],
+ '90.width': [7.96, 7.26],
+ '90.x': [771.96, 758.64],
+ '90.y': [101.24, 92.37],
+ '91.height': [29.4, 26.82],
+ '91.width': [13.4, 12.23],
+ '91.x': [762.88, 750.36],
+ '91.y': [89.68, 81.82],
+ '92.height': [16.92, 15.44],
+ '92.width': [13.11, 11.96],
+ '92.x': [766.07, 753.27],
+ '92.y': [85.69, 78.18],
+ '93.height': [36.11, 32.94],
+ '93.width': [14.05, 12.82],
+ '93.x': [754.23, 742.46],
+ '93.y': [74.42, 67.9],
+ '94.height': [29.24, 26.68],
+ '94.width': [14.56, 13.28],
+ '94.x': [768.1, 755.12],
+ '94.y': [83.97, 76.61],
+ '95.height': [9.28, 8.47],
+ '95.width': [6.89, 6.29],
+ '95.x': [785.76, 771.23],
+ '95.y': [47.94, 43.74],
+ '96.height': [7.6, 6.93],
+ '96.width': [6.59, 6.01],
+ '96.x': [790.54, 775.59],
+ '96.y': [55.55, 50.68],
+ '97.height': [17.11, 15.61],
+ '97.width': [42.87, 39.11],
+ '97.x': [729.51, 719.91],
+ '97.y': [33.22, 30.3],
+ '98.height': [53.34, 48.67],
+ '98.width': [40, 36.5],
+ '98.x': [718.41, 709.78],
+ '98.y': [61.14, 55.78],
+ '99.height': [11.18, 10.2],
+ '99.width': [8.45, 7.71],
+ '99.x': [746.35, 735.28],
+ '99.y': [78.01, 71.17],
+ '100.height': [3, 2.74],
+ '100.width': [4.83, 4.41],
+ '100.x': [746.44, 735.36],
+ '100.y': [74.49, 67.96],
+ '101.height': [7.62, 6.95],
+ '101.width': [12.51, 11.42],
+ '101.x': [733.3, 723.37],
+ '101.y': [70.49, 64.31],
+ '102.height': [25.13, 22.92],
+ '102.width': [20.83, 19.01],
+ '102.x': [705.73, 698.21],
+ '102.y': [58.18, 53.08],
+ '103.height': [16.79, 15.32],
+ '103.width': [18.28, 16.68],
+ '103.x': [703.67, 696.34],
+ '103.y': [55.77, 50.89],
+ '104.height': [7.47, 6.81],
+ '104.width': [11.14, 10.16],
+ '104.x': [710.4, 702.48],
+ '104.y': [51.43, 46.92],
+ '105.height': [6.96, 6.35],
+ '105.width': [12.72, 11.6],
+ '105.x': [712.69, 704.56],
+ '105.y': [47.41, 43.26],
+ '106.height': [13.2, 12.05],
+ '106.width': [20.91, 19.08],
+ '106.x': [689.21, 683.14],
+ '106.y': [48.34, 44.11],
+ '107.height': [27.03, 24.66],
+ '107.width': [31.32, 28.57],
+ '107.x': [679.02, 673.85],
+ '107.y': [53.61, 48.91],
+ '108.height': [8.96, 8.17],
+ '108.width': [8.24, 7.52],
+ '108.x': [669.18, 664.87],
+ '108.y': [58.01, 52.93],
+ '109.height': [4.39, 4.01],
+ '109.width': [4.77, 4.35],
+ '109.x': [677.71, 672.65],
+ '109.y': [50.93, 46.47],
+ '110.height': [17.51, 15.98],
+ '110.width': [12.82, 11.7],
+ '110.x': [632.64, 631.53],
+ '110.y': [10.73, 9.79],
+ '111.height': [7.27, 6.64],
+ '111.width': [11.25, 10.26],
+ '111.x': [648.19, 645.72],
+ '111.y': [27.07, 24.69],
+ '112.height': [11.28, 10.29],
+ '112.width': [22.46, 20.49],
+ '112.x': [647.75, 645.32],
+ '112.y': [32.77, 29.9],
+ '113.height': [8.96, 8.17],
+ '113.width': [12.67, 11.56],
+ '113.x': [636.97, 635.49],
+ '113.y': [28.99, 26.45],
+ '114.height': [4.23, 3.86],
+ '114.width': [9.35, 8.53],
+ '114.x': [632.04, 630.98],
+ '114.y': [37.93, 34.61],
+ '115.height': [4.68, 4.27],
+ '115.width': [8.05, 7.35],
+ '115.x': [640.61, 638.8],
+ '115.y': [38.6, 35.21],
+ '116.height': [4.88, 4.45],
+ '116.width': [4.81, 4.39],
+ '116.x': [653.5, 650.57],
+ '116.y': [38.85, 35.44],
+ '117.height': [7.5, 6.84],
+ '117.width': [12.33, 11.25],
+ '117.x': [645.86, 643.59],
+ '117.y': [39.25, 35.81],
+ '118.height': [3.62, 3.3],
+ '118.width': [6.76, 6.17],
+ '118.x': [644.79, 642.62],
+ '118.y': [26.77, 24.43],
+ '119.height': [3.36, 3.07],
+ '119.width': [8.47, 7.73],
+ '119.x': [644.66, 642.5],
+ '119.y': [24.51, 22.36],
+ '120.height': [2.96, 2.7],
+ '120.width': [5.44, 4.96],
+ '120.x': [646.67, 644.33],
+ '120.y': [22.25, 20.3],
+ '121.height': [11.94, 10.89],
+ '121.width': [11.15, 10.17],
+ '121.x': [627.32, 626.68],
+ '121.y': [28.8, 26.27],
+ '122.height': [5.13, 4.68],
+ '122.width': [8.06, 7.35],
+ '122.x': [649.1, 646.55],
+ '122.y': [45.82, 41.81],
+ '123.height': [12.25, 11.18],
+ '123.width': [9.1, 8.3],
+ '123.x': [646.93, 644.57],
+ '123.y': [49.93, 45.55],
+ '124.height': [11.16, 10.18],
+ '124.width': [26.3, 24],
+ '124.x': [654.42, 651.4],
+ '124.y': [49.39, 45.06],
+ '125.height': [5.31, 4.85],
+ '125.width': [2.52, 2.3],
+ '125.x': [645.38, 643.15],
+ '125.y': [48.45, 44.2],
+ '126.height': [6.76, 6.17],
+ '126.width': [7.39, 6.74],
+ '126.x': [637.57, 636.03],
+ '126.y': [42.05, 38.36],
+ '127.height': [3.37, 3.07],
+ '127.width': [5.59, 5.1],
+ '127.x': [627.69, 627.01],
+ '127.y': [39.88, 36.38],
+ '128.height': [1.09, 0.99],
+ '128.width': [0.69, 0.63],
+ '128.x': [627.07, 626.45],
+ '128.y': [36.2, 33.03],
+ '129.height': [3.05, 2.78],
+ '129.width': [4.5, 4.11],
+ '129.x': [623.09, 622.82],
+ '129.y': [34.1, 31.11],
+ '130.height': [4.15, 3.79],
+ '130.width': [4.5, 4.11],
+ '130.x': [624.07, 623.71],
+ '130.y': [30.99, 28.27],
+ '131.height': [9.56, 8.73],
+ '131.width': [4.36, 3.98],
+ '131.x': [607.2, 608.32],
+ '131.y': [49.15, 44.84],
+ '132.height': [13.67, 12.47],
+ '132.width': [16.29, 14.86],
+ '132.x': [607.72, 608.8],
+ '132.y': [46.65, 42.56],
+ '133.height': [5.22, 4.76],
+ '133.width': [4.91, 4.48],
+ '133.x': [607.8, 608.87],
+ '133.y': [28.58, 26.07],
+ '134.height': [4.48, 4.09],
+ '134.width': [2.99, 2.73],
+ '134.x': [859.5, 838.51],
+ '134.y': [170.91, 155.93],
+ '135.height': [8.52, 7.77],
+ '135.width': [7.59, 6.93],
+ '135.x': [854.64, 834.07],
+ '135.y': [143.96, 131.34],
+ '136.height': [21.22, 19.36],
+ '136.width': [28.37, 25.88],
+ '136.x': [833, 814.33],
+ '136.y': [198.17, 180.8],
+ '137.height': [62.21, 56.76],
+ '137.width': [60.8, 55.47],
+ '137.x': [778.57, 764.67],
+ '137.y': [152.16, 138.82],
+ '138.height': [7.77, 7.09],
+ '138.width': [3.23, 2.94],
+ '138.x': [739.31, 728.85],
+ '138.y': [110.74, 101.04],
+ '139.height': [62.98, 57.46],
+ '139.width': [74.08, 67.58],
+ '139.x': [718.49, 709.86],
+ '139.y': [31.07, 28.34],
+ '140.height': [6.43, 5.87],
+ '140.width': [2.01, 1.83],
+ '140.x': [793.45, 778.25],
+ '140.y': [80.22, 73.19],
+ '141.height': [18.06, 16.48],
+ '141.width': [15.92, 14.52],
+ '141.x': [628.7, 627.94],
+ '141.y': [41.04, 37.45],
+ '142.height': [4.22, 3.85],
+ '142.width': [5.52, 5.03],
+ '142.x': [629.48, 628.65],
+ '142.y': [24.84, 22.67],
+ '143.height': [12.88, 11.75],
+ '143.width': [11.12, 10.14],
+ '143.x': [610.92, 611.72],
+ '143.y': [23.59, 21.52],
+ '144.height': [3.62, 3.31],
+ '144.width': [11.39, 10.39],
+ '144.x': [593.98, 596.26],
+ '144.y': [13.57, 12.38],
+ '145.height': [6.28, 5.73],
+ '145.width': [7.59, 6.93],
+ '145.x': [679.49, 674.28],
+ '145.y': [49.87, 45.5],
+ '146.height': [4.27, 3.9],
+ '146.width': [9.63, 8.79],
+ '146.x': [672.08, 667.52],
+ '146.y': [46.98, 42.86],
+ '147.height': [25.86, 23.59],
+ '147.width': [14.26, 13.01],
+ '147.x': [795.38, 780.01],
+ '147.y': [93.44, 85.25],
+ '148.height': [12.46, 11.37],
+ '148.width': [28.8, 26.28],
+ '148.x': [770.11, 756.95],
+ '148.y': [116.58, 106.36],
+ '149.height': [2.91, 2.66],
+ '149.width': [1.76, 1.6],
+ '149.x': [791.55, 776.52],
+ '149.y': [119.57, 109.09],
+ '150.height': [2.31, 2.11],
+ '150.width': [3.6, 3.28],
+ '150.x': [637.53, 635.99],
+ '150.y': [41.47, 37.84],
+ '151.height': [12.3, 11.22],
+ '151.width': [13.42, 12.24],
+ '151.x': [641.28, 639.41],
+ '151.y': [9.63, 8.79],
+ '152.height': [2.91, 2.66],
+ '152.width': [7, 6.39],
+ '152.x': [641.2, 639.34],
+ '152.y': [37.08, 33.83],
+ '153.height': [4.06, 3.7],
+ '153.width': [8.33, 7.6],
+ '153.x': [635.15, 633.83],
+ '153.y': [34.65, 31.62],
+ '154.height': [11.02, 10.05],
+ '154.width': [10.28, 9.38],
+ '154.x': [673.77, 669.06],
+ '154.y': [94.44, 86.16],
+ '155.height': [25.73, 23.48],
+ '155.width': [13.15, 12],
+ '155.x': [799.23, 783.52],
+ '155.y': [43.62, 39.8],
+ '156.height': [15.88, 14.49],
+ '156.width': [12.81, 11.69],
+ '156.x': [529.07, 537.04],
+ '156.y': [169.41, 154.56],
+ '157.height': [12.73, 11.62],
+ '157.width': [15.27, 13.94],
+ '157.x': [683.02, 677.49],
+ '157.y': [92.47, 84.36],
+ '158.height': [30.46, 27.79],
+ '158.width': [31.67, 28.89],
+ '158.x': [669.15, 664.84],
+ '158.y': [67.25, 61.36],
+ '159.height': [18.67, 17.03],
+ '159.width': [334.06, 304.78],
+ '159.x': [457.64, 471.87],
+ '159.y': [243.73, 222.37],
+ '160.height': [1.22, 1.11],
+ '160.width': [2.39, 2.18],
+ '160.x': [664.9, 660.96],
+ '160.y': [60.82, 55.49],
+ '161.height': [1.1, 1],
+ '161.width': [2.43, 2.21],
+ '161.x': [664.26, 660.38],
+ '161.y': [61.72, 56.31],
+ '162.height': [27.06, 24.69],
+ '162.width': [23.19, 21.16],
+ '162.x': [595.25, 597.42],
+ '162.y': [60.66, 55.34],
+ '163.height': [18.27, 16.67],
+ '163.width': [18.8, 17.15],
+ '163.x': [654.72, 651.68],
+ '163.y': [68.32, 62.33],
+ '164.height': [25.88, 23.61],
+ '164.width': [23.2, 21.17],
+ '164.x': [633.32, 632.15],
+ '164.y': [65.45, 59.71],
+ '165.height': [23.21, 21.17],
+ '165.width': [22.2, 20.26],
+ '165.x': [669.36, 665.03],
+ '165.y': [100.46, 91.66],
+ '166.height': [3.55, 3.24],
+ '166.width': [2.38, 2.17],
+ '166.x': [682.06, 676.62],
+ '166.y': [104.97, 95.77],
+ '167.height': [6.97, 6.36],
+ '167.width': [9.64, 8.79],
+ '167.x': [683.5, 677.94],
+ '167.y': [107.45, 98.03],
+ '168.height': [11.54, 10.52],
+ '168.width': [8.2, 7.48],
+ '168.x': [664.49, 660.59],
+ '168.y': [121.99, 111.3],
+ '169.height': [3.61, 3.29],
+ '169.width': [2.71, 2.47],
+ '169.x': [663.64, 659.81],
+ '169.y': [132.9, 121.25],
+ '170.height': [4.37, 3.98],
+ '170.width': [5.11, 4.66],
+ '170.x': [640.33, 638.55],
+ '170.y': [44.15, 40.28],
+ '171.height': [2.55, 2.33],
+ '171.width': [3.32, 3.03],
+ '171.x': [647.02, 644.65],
+ '171.y': [49.08, 44.78],
+ '172.height': [6.62, 6.04],
+ '172.width': [5.89, 5.37],
+ '172.x': [644.12, 642],
+ '172.y': [42.59, 38.86],
+ '173.height': [2.81, 2.57],
+ '173.width': [2.4, 2.19],
+ '173.x': [644.21, 642.08],
+ '173.y': [47.03, 42.91],
+ '174.height': [2.44, 2.23],
+ '174.width': [2.21, 2.02],
+ '174.x': [646.32, 644.01],
+ '174.y': [47.45, 43.29],
+ '175.height': [1.79, 1.63],
+ '175.width': [1.68, 1.53],
+ '175.x': [527.49, 535.6],
+ '175.y': [108.6, 99.08],
+ '176.height': [17.61, 16.07],
+ '176.width': [17.24, 15.73],
+ '176.x': [655.74, 652.61],
+ '176.y': [105.88, 96.6],
+ },
+ legend: {
+ 'continuousLegend.bounds.height': [46, 69],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-maps.stories.tsx.usBubblesSpec @390': {
+ reason:
+ 'Approved B3 static map captions and neutral-class context reserve measured key space; occupied-bounds size-key placement has no legacy layout opt-out.',
+ changes: {
+ legend: {
+ 'pointSizeLegend.bounds.height': [68.6, 69],
+ 'pointSizeLegend.bounds.width': [83.57, 84],
+ 'pointSizeLegend.bounds.x': [278.43, 278],
+ 'pointSizeLegend.bounds.y': [257.4, 257],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-maps.stories.tsx.usBubblesSpec @1280': {
+ reason:
+ 'Approved B3 static map captions and neutral-class context reserve measured key space; occupied-bounds size-key placement has no legacy layout opt-out.',
+ changes: {
+ legend: {
+ 'pointSizeLegend.bounds.height': [68.6, 69],
+ 'pointSizeLegend.bounds.width': [83.57, 84],
+ 'pointSizeLegend.bounds.x': [1158.43, 1158],
+ 'pointSizeLegend.bounds.y': [249.8, 249.4],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-maps.stories.tsx.worldDivergingSpec @390': {
+ reason:
+ 'Approved B3 static map captions and neutral-class context reserve measured key space; occupied-bounds size-key placement has no legacy layout opt-out.',
+ changes: {
+ dims: {
+ 'mapSize.height': [199.1, 150.1],
+ },
+ area: {
+ 'rect.height': [199.1, 150.1],
+ 'rect.y': [157.3, 206.3],
+ },
+ marks: {
+ '0.height': [3.12, 2.57],
+ '0.width': [370, 304],
+ '0.x': [0, 33],
+ '0.y': [121.59, 93.16],
+ '1.height': [15.14, 12.44],
+ '1.width': [11.22, 9.21],
+ '1.x': [215.68, 210.21],
+ '1.y': [100.48, 75.81],
+ '2.height': [8.94, 7.34],
+ '2.width': [8.72, 7.17],
+ '2.x': [167.7, 170.79],
+ '2.y': [60.91, 43.31],
+ '3.height': [34.84, 28.62],
+ '3.width': [72.43, 59.51],
+ '3.x': [73.3, 93.23],
+ '3.y': [8.32, 0.09],
+ '4.height': [58.61, 48.15],
+ '4.width': [101.55, 83.44],
+ '4.x': [23.52, 52.32],
+ '4.y': [14.09, 4.83],
+ '5.height': [16.53, 13.58],
+ '5.width': [35.35, 29.05],
+ '5.x': [225.6, 218.36],
+ '5.y': [27.84, 16.14],
+ '6.height': [10.17, 8.35],
+ '6.width': [17.24, 14.16],
+ '6.x': [235.21, 226.25],
+ '6.y': [38.54, 24.92],
+ '7.height': [11.46, 9.42],
+ '7.width': [16.08, 13.21],
+ '7.x': [331.82, 305.63],
+ '7.y': [102.66, 77.61],
+ '8.height': [22.3, 18.32],
+ '8.width': [48.02, 39.45],
+ '8.x': [284.59, 266.83],
+ '8.y': [91.41, 68.36],
+ '9.height': [40.74, 33.47],
+ '9.width': [15.53, 12.76],
+ '9.x': [116.21, 128.48],
+ '9.y': [129.55, 99.7],
+ '10.height': [46.92, 38.55],
+ '10.width': [16.45, 13.51],
+ '10.x': [113.09, 125.92],
+ '10.y': [123.74, 94.92],
+ '11.height': [26.03, 21.39],
+ '11.width': [19.91, 16.36],
+ '11.x': [197.73, 195.46],
+ '11.y': [91.72, 68.62],
+ '12.height': [19.28, 15.84],
+ '12.width': [10.13, 8.32],
+ '12.x': [227.9, 220.25],
+ '12.y': [82.23, 60.82],
+ '13.height': [14.36, 11.8],
+ '13.width': [8.29, 6.81],
+ '13.x': [220.5, 214.17],
+ '13.y': [91.37, 68.33],
+ '14.height': [19.05, 15.65],
+ '14.width': [16.57, 13.62],
+ '14.x': [207.71, 203.66],
+ '14.y': [68.49, 49.53],
+ '15.height': [22.09, 18.15],
+ '15.width': [10.61, 8.71],
+ '15.x': [198.97, 196.48],
+ '15.y': [66.59, 47.97],
+ '16.height': [2.58, 2.12],
+ '16.width': [3.09, 2.54],
+ '16.x': [108.91, 122.49],
+ '16.y': [71.33, 51.86],
+ '17.height': [3.14, 2.58],
+ '17.width': [3.7, 3.04],
+ '17.x': [111.54, 124.64],
+ '17.y': [71.37, 51.9],
+ '18.height': [34.84, 28.63],
+ '18.width': [273.11, 224.39],
+ '18.x': [50.05, 74.13],
+ '18.y': [8.94, 0.6],
+ '19.height': [4.45, 3.66],
+ '19.width': [2, 1.65],
+ '19.x': [106.46, 120.47],
+ '19.y': [61.73, 43.98],
+ '20.height': [1.29, 1.06],
+ '20.width': [2.81, 2.31],
+ '20.x': [132.93, 142.22],
+ '20.y': [165.92, 129.58],
+ '21.height': [16.01, 13.16],
+ '21.width': [18.35, 15.08],
+ '21.x': [188.86, 188.17],
+ '21.y': [9.15, 0.78],
+ '22.height': [15.09, 12.4],
+ '22.width': [40.31, 33.12],
+ '22.x': [136.87, 145.45],
+ '22.y': [8.21, 0],
+ '23.height': [1.28, 1.05],
+ '23.width': [1.89, 1.55],
+ '23.x': [244.5, 233.89],
+ '23.y': [163.19, 127.34],
+ '24.height': [1.57, 1.29],
+ '24.width': [2.55, 2.09],
+ '24.x': [315.14, 291.92],
+ '24.y': [110.79, 84.28],
+ '25.height': [16.74, 13.76],
+ '25.width': [16.5, 13.56],
+ '25.x': [201.12, 198.24],
+ '25.y': [129.9, 99.98],
+ '26.height': [2.61, 2.15],
+ '26.width': [2.36, 1.94],
+ '26.x': [211.46, 206.74],
+ '26.y': [138.66, 107.18],
+ '27.height': [24.43, 20.07],
+ '27.width': [25.91, 21.29],
+ '27.x': [71.16, 91.46],
+ '27.y': [54.31, 37.88],
+ '28.height': [6.23, 5.12],
+ '28.width': [5.09, 4.19],
+ '28.x': [128.57, 138.63],
+ '28.y': [140.58, 108.76],
+ '29.height': [53.56, 44.01],
+ '29.width': [40.94, 33.64],
+ '29.x': [107.83, 121.59],
+ '29.y': [91.74, 68.63],
+ '30.height': [18.08, 14.86],
+ '30.width': [13.41, 11.02],
+ '30.x': [112.81, 125.69],
+ '30.y': [112.87, 86],
+ '31.height': [25.58, 21.01],
+ '31.width': [14.32, 11.77],
+ '31.x': [99.87, 115.06],
+ '31.y': [99.21, 74.77],
+ '32.height': [23.54, 19.34],
+ '32.width': [12.68, 10.42],
+ '32.x': [102.28, 117.04],
+ '32.y': [81.66, 60.35],
+ '33.height': [3.35, 2.75],
+ '33.width': [5.93, 4.88],
+ '33.x': [98.53, 113.96],
+ '33.y': [85.61, 63.59],
+ '34.height': [4.19, 3.44],
+ '34.width': [3.35, 2.76],
+ '34.x': [95.76, 111.68],
+ '34.y': [83.36, 61.75],
+ '35.height': [5.97, 4.91],
+ '35.width': [5.05, 4.15],
+ '35.x': [94.29, 110.47],
+ '35.y': [78.07, 57.4],
+ '36.height': [4.19, 3.44],
+ '36.width': [6.51, 5.35],
+ '36.x': [92.82, 109.27],
+ '36.y': [76.7, 56.28],
+ '37.height': [1.77, 1.46],
+ '37.width': [2.46, 2.02],
+ '37.x': [91.93, 108.53],
+ '37.y': [78.89, 58.08],
+ '38.height': [5.65, 4.64],
+ '38.width': [4.37, 3.59],
+ '38.x': [89.88, 106.85],
+ '38.y': [74.2, 54.22],
+ '39.height': [3.6, 2.96],
+ '39.width': [1.72, 1.41],
+ '39.x': [93.25, 109.62],
+ '39.y': [73.27, 53.46],
+ '40.height': [16.07, 13.2],
+ '40.width': [14.04, 11.54],
+ '40.x': [108.69, 122.3],
+ '40.y': [82.04, 60.66],
+ '41.height': [9.99, 8.21],
+ '41.width': [4.95, 4.07],
+ '41.x': [120.85, 132.29],
+ '41.y': [87.35, 65.03],
+ '42.height': [5.93, 4.87],
+ '42.width': [4.34, 3.57],
+ '42.x': [124.28, 135.11],
+ '42.y': [90.64, 67.73],
+ '43.height': [63.94, 52.53],
+ '43.width': [65.84, 54.09],
+ '43.x': [127.91, 138.1],
+ '43.y': [32.3, 19.8],
+ '44.height': [8.94, 7.35],
+ '44.width': [5.98, 4.91],
+ '44.x': [100.23, 115.35],
+ '44.y': [97.18, 73.11],
+ '45.height': [0.79, 0.65],
+ '45.width': [1.68, 1.38],
+ '45.x': [116.27, 128.53],
+ '45.y': [73.24, 53.43],
+ '46.height': [1.13, 0.93],
+ '46.width': [2.12, 1.74],
+ '46.x': [104.92, 119.21],
+ '46.y': [73.24, 53.43],
+ '47.height': [4.53, 3.72],
+ '47.width': [10.54, 8.66],
+ '47.x': [99.08, 114.41],
+ '47.y': [66.89, 48.21],
+ '48.height': [9.27, 7.61],
+ '48.width': [7.85, 6.45],
+ '48.x': [210.86, 206.24],
+ '48.y': [120.88, 92.57],
+ '49.height': [12.41, 10.19],
+ '49.width': [9.82, 8.07],
+ '49.x': [204.92, 201.36],
+ '49.y': [123.85, 95.01],
+ '50.height': [16.33, 13.41],
+ '50.width': [13.65, 11.22],
+ '50.x': [197.02, 194.88],
+ '50.y': [122.86, 94.2],
+ '51.height': [5.92, 4.86],
+ '51.width': [6.3, 5.18],
+ '51.x': [166.83, 170.07],
+ '51.y': [75.89, 55.61],
+ '52.height': [20.44, 16.79],
+ '52.width': [16.93, 13.91],
+ '52.x': [172.45, 174.69],
+ '52.y': [64.49, 46.24],
+ '53.height': [17.37, 14.27],
+ '53.width': [12.38, 10.17],
+ '53.x': [167.7, 170.79],
+ '53.y': [61.26, 43.59],
+ '54.height': [8.54, 7.01],
+ '54.width': [3.14, 2.58],
+ '54.x': [185.8, 185.66],
+ '54.y': [81.94, 60.58],
+ '55.height': [16.24, 13.34],
+ '55.width': [15.85, 13.02],
+ '55.x': [185.31, 185.25],
+ '55.y': [66.5, 47.9],
+ '56.height': [13.48, 11.08],
+ '56.width': [12.3, 10.1],
+ '56.x': [187.81, 187.31],
+ '56.y': [79.67, 58.72],
+ '57.height': [15.63, 12.84],
+ '57.width': [7.89, 6.48],
+ '57.x': [193.88, 192.29],
+ '57.y': [81.07, 59.87],
+ '58.height': [7.14, 5.87],
+ '58.width': [2, 1.64],
+ '58.x': [184.95, 184.96],
+ '58.y': [83.64, 61.97],
+ '59.height': [8.97, 7.37],
+ '59.width': [4.5, 3.69],
+ '59.x': [181.61, 182.22],
+ '59.y': [83.52, 61.88],
+ '60.height': [8.69, 7.14],
+ '60.width': [6.31, 5.19],
+ '60.x': [176.02, 177.62],
+ '60.y': [84.33, 62.54],
+ '61.height': [7.39, 6.07],
+ '61.width': [7.55, 6.2],
+ '61.x': [169.29, 172.1],
+ '61.y': [81.45, 60.18],
+ '62.height': [2.22, 1.82],
+ '62.width': [3.09, 2.54],
+ '62.x': [167.73, 170.81],
+ '62.y': [81.39, 60.13],
+ '63.height': [5.89, 4.84],
+ '63.width': [4.06, 3.34],
+ '63.x': [173.06, 175.19],
+ '63.y': [87.11, 64.83],
+ '64.height': [4.58, 3.76],
+ '64.width': [3.13, 2.58],
+ '64.x': [171.21, 173.67],
+ '64.y': [84.99, 63.09],
+ '65.height': [7.67, 6.3],
+ '65.width': [7.93, 6.52],
+ '65.x': [179.32, 180.33],
+ '65.y': [77.93, 57.29],
+ '66.height': [12.47, 10.25],
+ '66.width': [13.5, 11.09],
+ '66.x': [200.11, 197.42],
+ '66.y': [83.46, 61.83],
+ '67.height': [12.36, 10.16],
+ '67.width': [7.7, 6.33],
+ '67.x': [196.61, 194.54],
+ '67.y': [93.88, 70.39],
+ '68.height': [8.89, 7.31],
+ '68.width': [5.9, 4.84],
+ '68.x': [194.21, 192.57],
+ '68.y': [95.85, 72.01],
+ '69.height': [1.8, 1.48],
+ '69.width': [2.07, 1.7],
+ '69.x': [194.74, 193.01],
+ '69.y': [95.91, 72.06],
+ '70.height': [13.52, 11.11],
+ '70.width': [12.3, 10.1],
+ '70.x': [207.49, 203.48],
+ '70.y': [110.74, 84.24],
+ '71.height': [10.53, 8.65],
+ '71.width': [3.03, 2.49],
+ '71.x': [218.77, 212.74],
+ '71.y': [112.13, 85.39],
+ '72.height': [22.49, 18.48],
+ '72.width': [10.93, 8.98],
+ '72.x': [216.11, 210.56],
+ '72.y': [113.65, 86.64],
+ '73.height': [2.16, 1.78],
+ '73.width': [1.38, 1.13],
+ '73.x': [215.48, 210.05],
+ '73.y': [134.7, 103.93],
+ '74.height': [18.83, 15.47],
+ '74.width': [13, 10.68],
+ '74.x': [196.95, 194.82],
+ '74.y': [105.39, 79.85],
+ '75.height': [3.03, 2.49],
+ '75.width': [1.8, 1.48],
+ '75.x': [215.38, 209.96],
+ '75.y': [102.45, 77.43],
+ '76.height': [4.89, 4.02],
+ '76.width': [1.25, 1.03],
+ '76.x': [218.38, 212.43],
+ '76.y': [53.59, 37.29],
+ '77.height': [1.99, 1.63],
+ '77.width': [1.23, 1.01],
+ '77.x': [218.91, 212.86],
+ '77.y': [51.85, 35.86],
+ '78.height': [18.56, 15.25],
+ '78.width': [8.28, 6.8],
+ '78.x': [228.7, 220.9],
+ '78.y': [116.06, 88.61],
+ '79.height': [1.52, 1.25],
+ '79.width': [0.57, 0.47],
+ '79.x': [218.97, 212.91],
+ '79.y': [54.55, 38.08],
+ '80.height': [1.04, 0.85],
+ '80.width': [3.11, 2.56],
+ '80.x': [167.58, 170.69],
+ '80.y': [79.65, 58.7],
+ '81.height': [8.99, 7.38],
+ '81.width': [3.86, 3.17],
+ '81.x': [192.23, 190.94],
+ '81.y': [48.45, 33.06],
+ '82.height': [23.77, 19.53],
+ '82.width': [20.67, 16.99],
+ '82.x': [176.39, 177.93],
+ '82.y': [48.73, 33.3],
+ '83.height': [5.42, 4.46],
+ '83.width': [3.72, 3.06],
+ '83.x': [219.29, 213.17],
+ '83.y': [53.46, 37.18],
+ '84.height': [4.78, 3.93],
+ '84.width': [4.69, 3.85],
+ '84.x': [236.73, 227.5],
+ '84.y': [63.04, 45.05],
+ '85.height': [2.08, 1.71],
+ '85.width': [0.91, 0.75],
+ '85.x': [235.66, 226.62],
+ '85.y': [62.96, 44.99],
+ '86.height': [2.01, 1.65],
+ '86.width': [1.93, 1.59],
+ '86.x': [230.81, 222.64],
+ '86.y': [57.76, 40.71],
+ '87.height': [10.61, 8.72],
+ '87.width': [10.21, 8.39],
+ '87.x': [222.39, 215.72],
+ '87.y': [48.4, 33.03],
+ '88.height': [13.23, 10.87],
+ '88.width': [7.35, 6.04],
+ '88.x': [238.04, 228.58],
+ '88.y': [62.59, 44.68],
+ '89.height': [2.73, 2.24],
+ '89.width': [0.8, 0.65],
+ '89.x': [356.55, 325.95],
+ '89.y': [119.66, 91.57],
+ '90.height': [5.69, 4.67],
+ '90.width': [5.54, 4.55],
+ '90.x': [290.8, 271.93],
+ '90.y': [78.69, 57.91],
+ '91.height': [20.47, 16.82],
+ '91.width': [9.33, 7.67],
+ '91.x': [284.48, 266.73],
+ '91.y': [70.64, 51.3],
+ '92.height': [11.78, 9.68],
+ '92.width': [9.13, 7.5],
+ '92.x': [286.7, 268.56],
+ '92.y': [67.87, 49.02],
+ '93.height': [25.14, 20.65],
+ '93.width': [9.78, 8.04],
+ '93.x': [278.45, 261.78],
+ '93.y': [60.02, 42.57],
+ '94.height': [20.36, 16.73],
+ '94.width': [10.13, 8.33],
+ '94.x': [288.11, 269.72],
+ '94.y': [66.66, 48.03],
+ '95.height': [6.46, 5.31],
+ '95.width': [4.8, 3.94],
+ '95.x': [300.41, 279.82],
+ '95.y': [41.59, 27.43],
+ '96.height': [5.29, 4.35],
+ '96.width': [4.59, 3.77],
+ '96.x': [303.73, 282.55],
+ '96.y': [46.88, 31.78],
+ '97.height': [11.91, 9.79],
+ '97.width': [29.85, 24.52],
+ '97.x': [261.24, 247.64],
+ '97.y': [31.33, 19],
+ '98.height': [37.14, 30.51],
+ '98.width': [27.85, 22.88],
+ '98.x': [253.51, 241.29],
+ '98.y': [50.77, 34.97],
+ '99.height': [7.78, 6.39],
+ '99.width': [5.88, 4.83],
+ '99.x': [272.97, 257.28],
+ '99.y': [62.52, 44.63],
+ '100.height': [2.09, 1.72],
+ '100.width': [3.36, 2.76],
+ '100.x': [273.03, 257.33],
+ '100.y': [60.07, 42.61],
+ '101.height': [5.3, 4.36],
+ '101.width': [8.71, 7.16],
+ '101.x': [263.88, 249.81],
+ '101.y': [57.28, 40.32],
+ '102.height': [17.49, 14.37],
+ '102.width': [14.51, 11.92],
+ '102.x': [244.68, 234.04],
+ '102.y': [48.72, 33.28],
+ '103.height': [11.69, 9.6],
+ '103.width': [12.73, 10.46],
+ '103.x': [243.25, 232.86],
+ '103.y': [47.04, 31.9],
+ '104.height': [5.2, 4.27],
+ '104.width': [7.76, 6.37],
+ '104.x': [247.94, 236.71],
+ '104.y': [44.01, 29.42],
+ '105.height': [4.85, 3.98],
+ '105.width': [8.85, 7.27],
+ '105.x': [249.53, 238.02],
+ '105.y': [41.21, 27.12],
+ '106.height': [9.19, 7.55],
+ '106.width': [14.56, 11.96],
+ '106.x': [233.18, 224.59],
+ '106.y': [41.86, 27.65],
+ '107.height': [18.82, 15.46],
+ '107.width': [21.8, 17.92],
+ '107.x': [226.09, 218.76],
+ '107.y': [45.53, 30.67],
+ '108.height': [6.24, 5.12],
+ '108.width': [5.73, 4.71],
+ '108.x': [219.24, 213.13],
+ '108.y': [48.6, 33.18],
+ '109.height': [3.06, 2.51],
+ '109.width': [3.32, 2.73],
+ '109.x': [225.18, 218.01],
+ '109.y': [43.66, 29.13],
+ '110.height': [12.19, 10.02],
+ '110.width': [8.93, 7.33],
+ '110.x': [193.8, 192.23],
+ '110.y': [15.67, 6.14],
+ '111.height': [5.06, 4.16],
+ '111.width': [7.83, 6.43],
+ '111.x': [204.63, 201.12],
+ '111.y': [27.05, 15.48],
+ '112.height': [7.85, 6.45],
+ '112.width': [15.64, 12.85],
+ '112.x': [204.32, 200.87],
+ '112.y': [31.02, 18.75],
+ '113.height': [6.24, 5.13],
+ '113.width': [8.82, 7.25],
+ '113.x': [196.82, 194.71],
+ '113.y': [28.39, 16.58],
+ '114.height': [2.95, 2.42],
+ '114.width': [6.51, 5.35],
+ '114.x': [193.38, 191.88],
+ '114.y': [34.62, 21.7],
+ '115.height': [3.26, 2.68],
+ '115.width': [5.61, 4.61],
+ '115.x': [199.35, 196.79],
+ '115.y': [35.08, 22.08],
+ '116.height': [3.39, 2.79],
+ '116.width': [3.35, 2.75],
+ '116.x': [208.33, 204.17],
+ '116.y': [35.25, 22.22],
+ '117.height': [5.22, 4.29],
+ '117.width': [8.59, 7.05],
+ '117.x': [203, 199.79],
+ '117.y': [35.53, 22.45],
+ '118.height': [2.52, 2.07],
+ '118.width': [4.71, 3.87],
+ '118.x': [202.26, 199.18],
+ '118.y': [26.85, 15.32],
+ '119.height': [2.34, 1.92],
+ '119.width': [5.9, 4.85],
+ '119.x': [202.17, 199.11],
+ '119.y': [25.27, 14.02],
+ '120.height': [2.06, 1.69],
+ '120.width': [3.79, 3.11],
+ '120.x': [203.57, 200.25],
+ '120.y': [23.7, 12.73],
+ '121.height': [8.31, 6.83],
+ '121.width': [7.76, 6.38],
+ '121.x': [190.09, 189.19],
+ '121.y': [28.25, 16.47],
+ '122.height': [3.57, 2.93],
+ '122.width': [5.61, 4.61],
+ '122.x': [205.26, 201.65],
+ '122.y': [40.11, 26.21],
+ '123.height': [8.53, 7.01],
+ '123.width': [6.33, 5.2],
+ '123.x': [203.75, 200.41],
+ '123.y': [42.97, 28.56],
+ '124.height': [7.77, 6.38],
+ '124.width': [18.31, 15.05],
+ '124.x': [208.96, 204.69],
+ '124.y': [42.59, 28.25],
+ '125.height': [3.7, 3.04],
+ '125.width': [1.76, 1.44],
+ '125.x': [202.67, 199.52],
+ '125.y': [41.94, 27.71],
+ '126.height': [4.71, 3.87],
+ '126.width': [5.14, 4.22],
+ '126.x': [197.24, 195.05],
+ '126.y': [37.48, 24.05],
+ '127.height': [2.34, 1.93],
+ '127.width': [3.89, 3.2],
+ '127.x': [190.35, 189.4],
+ '127.y': [35.97, 22.81],
+ '128.height': [0.76, 0.62],
+ '128.width': [0.48, 0.39],
+ '128.x': [189.92, 189.04],
+ '128.y': [33.41, 20.71],
+ '129.height': [2.12, 1.75],
+ '129.width': [3.13, 2.57],
+ '129.x': [187.15, 186.77],
+ '129.y': [31.95, 19.51],
+ '130.height': [2.89, 2.38],
+ '130.width': [3.13, 2.58],
+ '130.x': [187.83, 187.33],
+ '130.y': [29.78, 17.73],
+ '131.height': [6.66, 5.47],
+ '131.width': [3.04, 2.49],
+ '131.x': [176.09, 177.68],
+ '131.y': [42.42, 28.11],
+ '132.height': [9.52, 7.82],
+ '132.width': [11.34, 9.32],
+ '132.x': [176.45, 177.98],
+ '132.y': [40.68, 26.68],
+ '133.height': [3.63, 2.99],
+ '133.width': [3.42, 2.81],
+ '133.x': [176.51, 178.02],
+ '133.y': [28.1, 16.35],
+ '134.height': [3.12, 2.56],
+ '134.width': [2.08, 1.71],
+ '134.x': [351.75, 322],
+ '134.y': [127.2, 97.77],
+ '135.height': [5.93, 4.87],
+ '135.width': [5.28, 4.34],
+ '135.x': [348.36, 319.22],
+ '135.y': [108.43, 82.35],
+ '136.height': [14.77, 12.14],
+ '136.width': [19.75, 16.23],
+ '136.x': [333.29, 306.84],
+ '136.y': [146.17, 113.36],
+ '137.height': [43.31, 35.59],
+ '137.width': [42.33, 34.78],
+ '137.x': [295.4, 275.71],
+ '137.y': [114.14, 87.04],
+ '138.height': [5.41, 4.45],
+ '138.width': [2.25, 1.85],
+ '138.x': [268.06, 253.25],
+ '138.y': [85.31, 63.35],
+ '139.height': [43.85, 36.03],
+ '139.width': [51.57, 42.37],
+ '139.x': [253.57, 241.34],
+ '139.y': [29.84, 17.77],
+ '140.height': [4.48, 3.68],
+ '140.width': [1.4, 1.15],
+ '140.x': [305.76, 284.22],
+ '140.y': [64.05, 45.89],
+ '141.height': [12.58, 10.33],
+ '141.width': [11.08, 9.1],
+ '141.x': [191.06, 189.98],
+ '141.y': [36.78, 23.48],
+ '142.height': [2.94, 2.41],
+ '142.width': [3.84, 3.16],
+ '142.x': [191.6, 190.42],
+ '142.y': [25.5, 14.21],
+ '143.height': [8.97, 7.37],
+ '143.width': [7.74, 6.36],
+ '143.x': [178.68, 179.81],
+ '143.y': [24.63, 13.49],
+ '144.height': [2.52, 2.07],
+ '144.width': [7.93, 6.51],
+ '144.x': [166.88, 170.11],
+ '144.y': [17.65, 7.76],
+ '145.height': [4.38, 3.59],
+ '145.width': [5.28, 4.34],
+ '145.x': [226.42, 219.03],
+ '145.y': [42.93, 28.53],
+ '146.height': [2.98, 2.44],
+ '146.width': [6.71, 5.51],
+ '146.x': [221.26, 214.79],
+ '146.y': [40.91, 26.87],
+ '147.height': [18, 14.79],
+ '147.width': [9.93, 8.16],
+ '147.x': [307.1, 285.32],
+ '147.y': [73.26, 53.45],
+ '148.height': [8.67, 7.13],
+ '148.width': [20.05, 16.48],
+ '148.x': [289.51, 270.87],
+ '148.y': [89.37, 66.69],
+ '149.height': [2.03, 1.67],
+ '149.width': [1.22, 1],
+ '149.x': [304.44, 283.13],
+ '149.y': [91.45, 68.4],
+ '150.height': [1.61, 1.32],
+ '150.width': [2.5, 2.06],
+ '150.x': [197.2, 195.03],
+ '150.y': [37.08, 23.72],
+ '151.height': [8.56, 7.03],
+ '151.width': [9.34, 7.68],
+ '151.x': [199.81, 197.17],
+ '151.y': [14.91, 5.51],
+ '152.height': [2.03, 1.67],
+ '152.width': [4.87, 4],
+ '152.x': [199.76, 197.13],
+ '152.y': [34.02, 21.21],
+ '153.height': [2.82, 2.32],
+ '153.width': [5.8, 4.77],
+ '153.x': [195.55, 193.67],
+ '153.y': [32.33, 19.82],
+ '154.height': [7.67, 6.3],
+ '154.width': [7.16, 5.88],
+ '154.x': [222.44, 215.76],
+ '154.y': [73.96, 54.02],
+ '155.height': [17.92, 14.72],
+ '155.width': [9.15, 7.52],
+ '155.x': [309.78, 287.52],
+ '155.y': [38.58, 24.95],
+ '156.height': [11.06, 9.09],
+ '156.width': [8.92, 7.33],
+ '156.x': [121.69, 132.99],
+ '156.y': [126.15, 96.91],
+ '157.height': [8.87, 7.28],
+ '157.width': [10.63, 8.74],
+ '157.x': [228.87, 221.05],
+ '157.y': [72.58, 52.89],
+ '158.height': [21.2, 17.42],
+ '158.width': [22.05, 18.11],
+ '158.x': [219.22, 213.11],
+ '158.y': [55.03, 38.47],
+ '159.height': [13, 10.68],
+ '159.width': [232.58, 191.09],
+ '159.x': [71.96, 92.12],
+ '159.y': [177.9, 139.42],
+ '160.height': [0.85, 0.7],
+ '160.width': [1.66, 1.37],
+ '160.x': [216.26, 210.68],
+ '160.y': [50.55, 34.79],
+ '161.height': [0.76, 0.63],
+ '161.width': [1.69, 1.39],
+ '161.x': [215.81, 210.32],
+ '161.y': [51.18, 35.31],
+ '162.height': [18.84, 15.48],
+ '162.width': [16.15, 13.27],
+ '162.x': [167.77, 170.84],
+ '162.y': [50.44, 34.7],
+ '163.height': [12.72, 10.45],
+ '163.width': [13.09, 10.75],
+ '163.x': [209.18, 204.86],
+ '163.y': [55.77, 39.08],
+ '164.height': [18.02, 14.8],
+ '164.width': [16.15, 13.27],
+ '164.x': [194.28, 192.62],
+ '164.y': [53.77, 37.44],
+ '165.height': [16.16, 13.27],
+ '165.width': [15.46, 12.7],
+ '165.x': [219.36, 213.23],
+ '165.y': [78.15, 57.47],
+ '166.height': [2.47, 2.03],
+ '166.width': [1.66, 1.36],
+ '166.x': [228.21, 220.5],
+ '166.y': [81.29, 60.05],
+ '167.height': [4.85, 3.99],
+ '167.width': [6.71, 5.51],
+ '167.x': [229.21, 221.33],
+ '167.y': [83.02, 61.47],
+ '168.height': [8.03, 6.6],
+ '168.width': [5.71, 4.69],
+ '168.x': [215.98, 210.45],
+ '168.y': [93.14, 69.78],
+ '169.height': [2.51, 2.07],
+ '169.width': [1.89, 1.55],
+ '169.x': [215.38, 209.96],
+ '169.y': [100.74, 76.02],
+ '170.height': [3.04, 2.5],
+ '170.width': [3.56, 2.92],
+ '170.x': [199.15, 196.63],
+ '170.y': [38.94, 25.25],
+ '171.height': [1.78, 1.46],
+ '171.width': [2.31, 1.9],
+ '171.x': [203.81, 200.46],
+ '171.y': [42.38, 28.08],
+ '172.height': [4.61, 3.79],
+ '172.width': [4.1, 3.37],
+ '172.x': [201.79, 198.8],
+ '172.y': [37.86, 24.36],
+ '173.height': [1.96, 1.61],
+ '173.width': [1.67, 1.38],
+ '173.x': [201.85, 198.85],
+ '173.y': [40.95, 26.9],
+ '174.height': [1.7, 1.4],
+ '174.width': [1.54, 1.27],
+ '174.x': [203.32, 200.05],
+ '174.y': [41.24, 27.15],
+ '175.height': [1.25, 1.02],
+ '175.width': [1.17, 0.96],
+ '175.x': [120.59, 132.08],
+ '175.y': [83.82, 62.12],
+ '176.height': [12.26, 10.08],
+ '176.width': [12, 9.86],
+ '176.x': [209.88, 205.45],
+ '176.y': [81.92, 60.57],
+ },
+ legend: {
+ 'continuousLegend.bounds.height': [61, 110],
+ 'continuousLegend.bar.height': [12, 15],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-maps.stories.tsx.worldDivergingSpec @1280': {
+ reason:
+ 'Approved B3 static map captions and neutral-class context reserve measured key space; occupied-bounds size-key placement has no legacy layout opt-out.',
+ changes: {
+ dims: {
+ 'mapSize.height': [198.2, 149.2],
+ },
+ area: {
+ 'rect.height': [198.2, 149.2],
+ 'rect.y': [138.2, 187.2],
+ },
+ marks: {
+ '0.height': [3.39, 2.55],
+ '0.width': [401.42, 302.18],
+ '0.x': [419.29, 468.91],
+ '0.y': [123.01, 92.6],
+ '1.height': [16.42, 12.36],
+ '1.width': [12.17, 9.16],
+ '1.x': [653.29, 645.06],
+ '1.y': [100.1, 75.36],
+ '2.height': [9.7, 7.3],
+ '2.width': [9.46, 7.12],
+ '2.x': [601.23, 605.87],
+ '2.y': [57.18, 43.05],
+ '3.height': [37.79, 28.45],
+ '3.width': [78.58, 59.15],
+ '3.x': [498.82, 528.78],
+ '3.y': [0.12, 0.09],
+ '4.height': [63.59, 47.87],
+ '4.width': [110.17, 82.94],
+ '4.x': [444.8, 488.12],
+ '4.y': [6.38, 4.8],
+ '5.height': [17.93, 13.5],
+ '5.width': [38.35, 28.87],
+ '5.x': [664.05, 653.16],
+ '5.y': [21.31, 16.04],
+ '6.height': [11.03, 8.3],
+ '6.width': [18.7, 14.08],
+ '6.x': [674.47, 661],
+ '6.y': [32.9, 24.77],
+ '7.height': [12.43, 9.36],
+ '7.width': [17.45, 13.13],
+ '7.x': [779.29, 739.91],
+ '7.y': [102.47, 77.14],
+ '8.height': [24.2, 18.21],
+ '8.width': [52.09, 39.21],
+ '8.x': [728.05, 701.34],
+ '8.y': [90.27, 67.95],
+ '9.height': [44.2, 33.27],
+ '9.width': [16.85, 12.68],
+ '9.x': [545.37, 563.82],
+ '9.y': [131.64, 99.1],
+ '10.height': [50.9, 38.32],
+ '10.width': [17.84, 13.43],
+ '10.x': [541.99, 561.27],
+ '10.y': [125.34, 94.35],
+ '11.height': [28.24, 21.26],
+ '11.width': [21.6, 16.26],
+ '11.x': [633.81, 630.39],
+ '11.y': [90.61, 68.21],
+ '12.height': [20.91, 15.74],
+ '12.width': [10.99, 8.27],
+ '12.x': [666.54, 655.03],
+ '12.y': [80.31, 60.46],
+ '13.height': [15.58, 11.73],
+ '13.width': [9, 6.77],
+ '13.x': [658.51, 648.99],
+ '13.y': [90.23, 67.92],
+ '14.height': [20.67, 15.56],
+ '14.width': [17.98, 13.54],
+ '14.x': [644.64, 638.55],
+ '14.y': [65.41, 49.24],
+ '15.height': [23.96, 18.04],
+ '15.width': [11.51, 8.66],
+ '15.x': [635.16, 631.41],
+ '15.y': [63.34, 47.68],
+ '16.height': [2.8, 2.11],
+ '16.width': [3.35, 2.52],
+ '16.x': [537.45, 557.86],
+ '16.y': [68.48, 51.55],
+ '17.height': [3.4, 2.56],
+ '17.width': [4.01, 3.02],
+ '17.x': [540.3, 560],
+ '17.y': [68.53, 51.59],
+ '18.height': [37.8, 28.46],
+ '18.width': [296.3, 223.05],
+ '18.x': [473.6, 509.79],
+ '18.y': [0.79, 0.6],
+ '19.height': [4.83, 3.63],
+ '19.width': [2.17, 1.64],
+ '19.x': [534.79, 555.85],
+ '19.y': [58.07, 43.71],
+ '20.height': [1.4, 1.05],
+ '20.width': [3.05, 2.29],
+ '20.x': [563.51, 577.48],
+ '20.y': [171.1, 128.8],
+ '21.height': [17.37, 13.08],
+ '21.width': [19.91, 14.99],
+ '21.x': [624.19, 623.15],
+ '21.y': [1.03, 0.77],
+ '22.height': [16.37, 12.32],
+ '22.width': [43.74, 32.92],
+ '22.x': [567.78, 580.69],
+ '23.height': [1.38, 1.04],
+ '23.width': [2.05, 1.54],
+ '23.x': [684.55, 668.59],
+ '23.y': [168.15, 126.58],
+ '24.height': [1.7, 1.28],
+ '24.width': [2.76, 2.08],
+ '24.x': [761.19, 726.28],
+ '24.y': [111.29, 83.78],
+ '25.height': [18.16, 13.67],
+ '25.width': [17.9, 13.48],
+ '25.x': [637.48, 633.16],
+ '25.y': [132.02, 99.38],
+ '26.height': [2.84, 2.14],
+ '26.width': [2.56, 1.93],
+ '26.x': [648.71, 641.61],
+ '26.y': [141.53, 106.54],
+ '27.height': [26.5, 19.95],
+ '27.width': [28.11, 21.16],
+ '27.x': [496.49, 527.03],
+ '27.y': [50.02, 37.65],
+ '28.height': [6.76, 5.09],
+ '28.width': [5.53, 4.16],
+ '28.x': [558.77, 573.91],
+ '28.y': [143.61, 108.11],
+ '29.height': [58.11, 43.74],
+ '29.width': [44.41, 33.43],
+ '29.x': [536.27, 556.97],
+ '29.y': [90.63, 68.22],
+ '30.height': [19.62, 14.77],
+ '30.width': [14.55, 10.95],
+ '30.x': [541.68, 561.04],
+ '30.y': [113.56, 85.48],
+ '31.height': [27.75, 20.89],
+ '31.width': [15.54, 11.7],
+ '31.x': [527.64, 550.48],
+ '31.y': [98.74, 74.33],
+ '32.height': [25.54, 19.23],
+ '32.width': [13.76, 10.35],
+ '32.x': [530.26, 552.45],
+ '32.y': [79.69, 59.99],
+ '33.height': [3.64, 2.74],
+ '33.width': [6.44, 4.85],
+ '33.x': [526.19, 549.38],
+ '33.y': [83.97, 63.21],
+ '34.height': [4.55, 3.42],
+ '34.width': [3.64, 2.74],
+ '34.x': [523.19, 547.12],
+ '34.y': [81.53, 61.38],
+ '35.height': [6.48, 4.88],
+ '35.width': [5.47, 4.12],
+ '35.x': [521.59, 545.92],
+ '35.y': [75.8, 57.06],
+ '36.height': [4.55, 3.42],
+ '36.width': [7.06, 5.32],
+ '36.x': [520, 544.72],
+ '36.y': [74.31, 55.94],
+ '37.height': [1.92, 1.45],
+ '37.width': [2.67, 2.01],
+ '37.x': [519.02, 543.99],
+ '37.y': [76.69, 57.73],
+ '38.height': [6.13, 4.61],
+ '38.width': [4.74, 3.57],
+ '38.x': [516.81, 542.32],
+ '38.y': [71.6, 53.9],
+ '39.height': [3.91, 2.94],
+ '39.width': [1.86, 1.4],
+ '39.x': [520.46, 545.07],
+ '39.y': [70.59, 53.14],
+ '40.height': [17.43, 13.12],
+ '40.width': [15.23, 11.47],
+ '40.x': [537.21, 557.68],
+ '40.y': [80.1, 60.3],
+ '41.height': [10.84, 8.16],
+ '41.width': [5.37, 4.04],
+ '41.x': [550.4, 567.61],
+ '41.y': [85.86, 64.64],
+ '42.height': [6.43, 4.84],
+ '42.width': [4.71, 3.55],
+ '42.x': [554.12, 570.41],
+ '42.y': [89.43, 67.32],
+ '43.height': [69.37, 52.22],
+ '43.width': [71.43, 53.77],
+ '43.x': [558.07, 573.38],
+ '43.y': [26.14, 19.68],
+ '44.height': [9.7, 7.3],
+ '44.width': [6.48, 4.88],
+ '44.x': [528.03, 550.77],
+ '44.y': [96.53, 72.67],
+ '45.height': [0.85, 0.64],
+ '45.width': [1.82, 1.37],
+ '45.x': [545.44, 563.87],
+ '45.y': [70.56, 53.11],
+ '46.height': [1.23, 0.92],
+ '46.width': [2.3, 1.73],
+ '46.x': [533.12, 554.6],
+ '46.y': [70.55, 53.11],
+ '47.height': [4.91, 3.7],
+ '47.width': [11.43, 8.61],
+ '47.x': [526.79, 549.83],
+ '47.y': [63.66, 47.92],
+ '48.height': [10.05, 7.57],
+ '48.width': [8.52, 6.41],
+ '48.x': [648.05, 641.12],
+ '48.y': [122.24, 92.02],
+ '49.height': [13.46, 10.13],
+ '49.width': [10.66, 8.02],
+ '49.x': [641.61, 636.27],
+ '49.y': [125.46, 94.44],
+ '50.height': [17.71, 13.33],
+ '50.width': [14.81, 11.15],
+ '50.x': [633.04, 629.82],
+ '50.y': [124.39, 93.63],
+ '51.height': [6.42, 4.83],
+ '51.width': [6.83, 5.14],
+ '51.x': [600.29, 605.16],
+ '51.y': [73.43, 55.27],
+ '52.height': [22.18, 16.69],
+ '52.width': [18.37, 13.83],
+ '52.x': [606.39, 609.75],
+ '52.y': [61.06, 45.96],
+ '53.height': [18.84, 14.18],
+ '53.width': [13.43, 10.11],
+ '53.x': [601.23, 605.87],
+ '53.y': [57.56, 43.33],
+ '54.height': [9.26, 6.97],
+ '54.width': [3.41, 2.56],
+ '54.x': [620.87, 620.66],
+ '54.y': [79.99, 60.22],
+ '55.height': [17.61, 13.26],
+ '55.width': [17.2, 12.94],
+ '55.x': [620.33, 620.25],
+ '55.y': [63.25, 47.61],
+ '56.height': [14.63, 11.01],
+ '56.width': [13.34, 10.04],
+ '56.x': [623.05, 622.3],
+ '56.y': [77.53, 58.36],
+ '57.height': [16.96, 12.76],
+ '57.width': [8.56, 6.44],
+ '57.x': [629.63, 627.25],
+ '57.y': [79.05, 59.51],
+ '58.height': [7.75, 5.83],
+ '58.width': [2.17, 1.63],
+ '58.x': [619.95, 619.96],
+ '58.y': [81.83, 61.6],
+ '59.height': [9.73, 7.32],
+ '59.width': [4.88, 3.67],
+ '59.x': [616.32, 617.23],
+ '59.y': [81.71, 61.51],
+ '60.height': [9.43, 7.1],
+ '60.width': [6.85, 5.16],
+ '60.x': [610.25, 612.66],
+ '60.y': [82.58, 62.17],
+ '61.height': [8.01, 6.03],
+ '61.width': [8.19, 6.16],
+ '61.x': [602.96, 607.17],
+ '61.y': [79.46, 59.82],
+ '62.height': [2.4, 1.81],
+ '62.width': [3.35, 2.52],
+ '62.x': [601.26, 605.89],
+ '62.y': [79.4, 59.77],
+ '63.height': [6.39, 4.81],
+ '63.width': [4.4, 3.32],
+ '63.x': [607.05, 610.25],
+ '63.y': [85.6, 64.44],
+ '64.height': [4.96, 3.74],
+ '64.width': [3.4, 2.56],
+ '64.x': [605.03, 608.73],
+ '64.y': [83.31, 62.71],
+ '65.height': [8.32, 6.27],
+ '65.width': [8.61, 6.48],
+ '65.x': [613.84, 615.36],
+ '65.y': [75.65, 56.95],
+ '66.height': [13.53, 10.19],
+ '66.width': [14.65, 11.03],
+ '66.x': [636.39, 632.34],
+ '66.y': [81.65, 61.46],
+ '67.height': [13.41, 10.1],
+ '67.width': [8.36, 6.29],
+ '67.x': [632.59, 629.48],
+ '67.y': [92.94, 69.97],
+ '68.height': [9.65, 7.26],
+ '68.width': [6.4, 4.82],
+ '68.x': [629.99, 627.52],
+ '68.y': [95.09, 71.58],
+ '69.height': [1.95, 1.47],
+ '69.width': [2.25, 1.69],
+ '69.x': [630.57, 627.96],
+ '69.y': [95.15, 71.63],
+ '70.height': [14.67, 11.04],
+ '70.width': [13.34, 10.04],
+ '70.x': [644.4, 638.37],
+ '70.y': [111.24, 83.74],
+ '71.height': [11.43, 8.6],
+ '71.width': [3.29, 2.47],
+ '71.x': [656.64, 647.58],
+ '71.y': [112.75, 84.87],
+ '72.height': [24.4, 18.37],
+ '72.width': [11.86, 8.93],
+ '72.x': [653.75, 645.41],
+ '72.y': [114.4, 86.12],
+ '73.height': [2.34, 1.77],
+ '73.width': [1.5, 1.13],
+ '73.x': [653.07, 644.9],
+ '73.y': [137.24, 103.31],
+ '74.height': [20.42, 15.37],
+ '74.width': [14.1, 10.62],
+ '74.x': [632.96, 629.76],
+ '74.y': [105.44, 79.37],
+ '75.height': [3.29, 2.48],
+ '75.width': [1.95, 1.47],
+ '75.x': [652.96, 644.81],
+ '75.y': [102.24, 76.97],
+ '76.height': [5.31, 4],
+ '76.width': [1.36, 1.02],
+ '76.x': [656.22, 647.26],
+ '76.y': [49.24, 37.07],
+ '77.height': [2.16, 1.62],
+ '77.width': [1.33, 1],
+ '77.x': [656.79, 647.69],
+ '77.y': [47.35, 35.64],
+ '78.height': [20.14, 15.16],
+ '78.width': [8.98, 6.76],
+ '78.x': [667.41, 655.69],
+ '78.y': [117.01, 88.08],
+ '79.height': [1.65, 1.24],
+ '79.width': [0.61, 0.46],
+ '79.x': [656.85, 647.74],
+ '79.y': [50.28, 37.85],
+ '80.height': [1.13, 0.85],
+ '80.width': [3.38, 2.54],
+ '80.x': [601.1, 605.77],
+ '80.y': [77.52, 58.35],
+ '81.height': [9.75, 7.34],
+ '81.width': [4.19, 3.15],
+ '81.x': [627.84, 625.9],
+ '81.y': [43.66, 32.86],
+ '82.height': [25.79, 19.41],
+ '82.width': [22.43, 16.88],
+ '82.x': [610.66, 612.97],
+ '82.y': [43.97, 33.1],
+ '83.height': [5.88, 4.43],
+ '83.width': [4.04, 3.04],
+ '83.x': [657.2, 648],
+ '83.y': [49.1, 36.96],
+ '84.height': [5.19, 3.9],
+ '84.width': [5.09, 3.83],
+ '84.x': [676.12, 662.25],
+ '84.y': [59.49, 44.78],
+ '85.height': [2.26, 1.7],
+ '85.width': [0.99, 0.75],
+ '85.x': [674.96, 661.37],
+ '85.y': [59.4, 44.72],
+ '86.height': [2.18, 1.64],
+ '86.width': [2.09, 1.58],
+ '86.x': [669.7, 657.41],
+ '86.y': [53.76, 40.47],
+ '87.height': [11.51, 8.67],
+ '87.width': [11.07, 8.34],
+ '87.x': [660.56, 650.54],
+ '87.y': [43.61, 32.83],
+ '88.height': [14.35, 10.8],
+ '88.width': [7.97, 6],
+ '88.x': [677.54, 663.32],
+ '88.y': [59, 44.41],
+ '89.height': [2.96, 2.23],
+ '89.width': [0.86, 0.65],
+ '89.x': [806.11, 760.1],
+ '89.y': [120.91, 91.02],
+ '90.height': [6.17, 4.65],
+ '90.width': [6.01, 4.52],
+ '90.x': [734.78, 706.41],
+ '90.y': [76.47, 57.56],
+ '91.height': [22.2, 16.71],
+ '91.width': [10.12, 7.62],
+ '91.x': [727.92, 701.24],
+ '91.y': [67.74, 50.99],
+ '92.height': [12.78, 9.62],
+ '92.width': [9.9, 7.46],
+ '92.x': [730.33, 703.05],
+ '92.y': [64.72, 48.72],
+ '93.height': [27.27, 20.53],
+ '93.width': [10.61, 7.99],
+ '93.x': [721.39, 696.32],
+ '93.y': [56.21, 42.31],
+ '94.height': [22.09, 16.63],
+ '94.width': [10.99, 8.28],
+ '94.x': [731.87, 704.21],
+ '94.y': [63.42, 47.74],
+ '95.height': [7.01, 5.28],
+ '95.width': [5.21, 3.92],
+ '95.x': [745.21, 714.25],
+ '95.y': [36.21, 27.26],
+ '96.height': [5.74, 4.32],
+ '96.width': [4.98, 3.75],
+ '96.x': [748.81, 716.97],
+ '96.y': [41.96, 31.58],
+ '97.height': [12.92, 9.73],
+ '97.width': [32.38, 24.37],
+ '97.x': [702.72, 682.27],
+ '97.y': [25.09, 18.89],
+ '98.height': [40.29, 30.33],
+ '98.width': [30.22, 22.75],
+ '98.x': [694.33, 675.95],
+ '98.y': [46.18, 34.76],
+ '99.height': [8.44, 6.36],
+ '99.width': [6.38, 4.81],
+ '99.x': [715.44, 691.84],
+ '99.y': [58.93, 44.36],
+ '100.height': [2.27, 1.71],
+ '100.width': [3.65, 2.75],
+ '100.x': [715.51, 691.9],
+ '100.y': [56.27, 42.36],
+ '101.height': [5.75, 4.33],
+ '101.width': [9.45, 7.12],
+ '101.x': [705.58, 684.42],
+ '101.y': [53.24, 40.08],
+ '102.height': [18.98, 14.29],
+ '102.width': [15.74, 11.85],
+ '102.x': [684.75, 668.74],
+ '102.y': [43.95, 33.08],
+ '103.height': [12.68, 9.55],
+ '103.width': [13.81, 10.4],
+ '103.x': [683.2, 667.57],
+ '103.y': [42.13, 31.71],
+ '104.height': [5.64, 4.25],
+ '104.width': [8.41, 6.33],
+ '104.x': [688.28, 671.4],
+ '104.y': [38.85, 29.24],
+ '105.height': [5.26, 3.96],
+ '105.width': [9.6, 7.23],
+ '105.x': [690.01, 672.7],
+ '105.y': [35.81, 26.96],
+ '106.height': [9.97, 7.51],
+ '106.width': [15.79, 11.89],
+ '106.x': [672.27, 659.35],
+ '106.y': [36.52, 27.49],
+ '107.height': [20.42, 15.37],
+ '107.width': [23.66, 17.81],
+ '107.x': [664.58, 653.56],
+ '107.y': [40.49, 30.48],
+ '108.height': [6.77, 5.09],
+ '108.width': [6.22, 4.68],
+ '108.x': [657.14, 647.96],
+ '108.y': [43.82, 32.99],
+ '109.height': [3.32, 2.5],
+ '109.width': [3.6, 2.71],
+ '109.x': [663.59, 652.81],
+ '109.y': [38.47, 28.96],
+ '110.height': [13.23, 9.96],
+ '110.width': [9.68, 7.29],
+ '110.x': [629.55, 627.19],
+ '110.y': [8.1, 6.1],
+ '111.height': [5.49, 4.14],
+ '111.width': [8.5, 6.4],
+ '111.x': [641.29, 636.03],
+ '111.y': [20.44, 15.39],
+ '112.height': [8.52, 6.41],
+ '112.width': [16.96, 12.77],
+ '112.x': [640.96, 635.78],
+ '112.y': [24.76, 18.64],
+ '113.height': [6.77, 5.09],
+ '113.width': [9.57, 7.21],
+ '113.x': [632.82, 629.65],
+ '113.y': [21.9, 16.48],
+ '114.height': [3.2, 2.41],
+ '114.width': [7.06, 5.32],
+ '114.x': [629.09, 626.84],
+ '114.y': [28.65, 21.57],
+ '115.height': [3.53, 2.66],
+ '115.width': [6.08, 4.58],
+ '115.x': [635.57, 631.72],
+ '115.y': [29.15, 21.95],
+ '116.height': [3.68, 2.77],
+ '116.width': [3.63, 2.73],
+ '116.x': [645.31, 639.05],
+ '116.y': [29.34, 22.09],
+ '117.height': [5.67, 4.27],
+ '117.width': [9.31, 7.01],
+ '117.x': [639.53, 634.7],
+ '117.y': [29.64, 22.31],
+ '118.height': [2.74, 2.06],
+ '118.width': [5.11, 3.85],
+ '118.x': [638.73, 634.1],
+ '118.y': [20.22, 15.22],
+ '119.height': [2.54, 1.91],
+ '119.width': [6.4, 4.82],
+ '119.x': [638.63, 634.02],
+ '119.y': [18.51, 13.93],
+ '120.height': [2.23, 1.68],
+ '120.width': [4.11, 3.09],
+ '120.x': [640.14, 635.16],
+ '120.y': [16.8, 12.65],
+ '121.height': [9.02, 6.79],
+ '121.width': [8.42, 6.34],
+ '121.x': [625.53, 624.16],
+ '121.y': [21.75, 16.37],
+ '122.height': [3.88, 2.92],
+ '122.width': [6.09, 4.58],
+ '122.x': [641.98, 636.55],
+ '122.y': [34.61, 26.06],
+ '123.height': [9.26, 6.97],
+ '123.width': [6.87, 5.17],
+ '123.x': [640.34, 635.31],
+ '123.y': [37.71, 28.39],
+ '124.height': [8.43, 6.35],
+ '124.width': [19.87, 14.96],
+ '124.x': [646, 639.57],
+ '124.y': [37.3, 28.08],
+ '125.height': [4.01, 3.02],
+ '125.width': [1.9, 1.43],
+ '125.x': [639.17, 634.43],
+ '125.y': [36.6, 27.55],
+ '126.height': [5.11, 3.84],
+ '126.width': [5.58, 4.2],
+ '126.x': [633.27, 629.99],
+ '126.y': [31.76, 23.91],
+ '127.height': [2.54, 1.91],
+ '127.width': [4.22, 3.18],
+ '127.x': [625.81, 624.37],
+ '127.y': [30.12, 22.67],
+ '128.height': [0.82, 0.62],
+ '128.width': [0.52, 0.39],
+ '128.x': [625.34, 624.02],
+ '128.y': [27.35, 20.59],
+ '129.height': [2.3, 1.74],
+ '129.width': [3.4, 2.56],
+ '129.x': [622.33, 621.76],
+ '129.y': [25.76, 19.39],
+ '130.height': [3.14, 2.36],
+ '130.width': [3.4, 2.56],
+ '130.x': [623.07, 622.31],
+ '130.y': [23.41, 17.62],
+ '131.height': [7.22, 5.44],
+ '131.width': [3.29, 2.48],
+ '131.x': [610.33, 612.72],
+ '131.y': [37.12, 27.95],
+ '132.height': [10.33, 7.77],
+ '132.width': [12.3, 9.26],
+ '132.x': [610.72, 613.02],
+ '132.y': [35.23, 26.52],
+ '133.height': [3.94, 2.97],
+ '133.width': [3.71, 2.79],
+ '133.x': [610.79, 613.07],
+ '133.y': [21.59, 16.25],
+ '134.height': [3.38, 2.55],
+ '134.width': [2.26, 1.7],
+ '134.x': [800.9, 756.18],
+ '134.y': [129.09, 97.18],
+ '135.height': [6.43, 4.84],
+ '135.width': [5.73, 4.32],
+ '135.x': [797.23, 753.42],
+ '135.y': [108.74, 81.86],
+ '136.height': [16.03, 12.06],
+ '136.width': [21.43, 16.13],
+ '136.x': [780.88, 741.11],
+ '136.y': [149.68, 112.68],
+ '137.height': [46.99, 35.37],
+ '137.width': [45.92, 34.57],
+ '137.x': [739.77, 710.16],
+ '137.y': [114.93, 86.52],
+ '138.height': [5.87, 4.42],
+ '138.width': [2.44, 1.83],
+ '138.x': [710.12, 687.84],
+ '138.y': [83.65, 62.97],
+ '139.height': [47.57, 35.81],
+ '139.width': [55.95, 42.12],
+ '139.x': [694.4, 676],
+ '139.y': [23.47, 17.66],
+ '140.height': [4.86, 3.66],
+ '140.width': [1.52, 1.14],
+ '140.x': [751.01, 718.62],
+ '140.y': [60.59, 45.61],
+ '141.height': [13.64, 10.27],
+ '141.width': [12.02, 9.05],
+ '141.x': [626.57, 624.95],
+ '141.y': [31, 23.34],
+ '142.height': [3.19, 2.4],
+ '142.width': [4.17, 3.14],
+ '142.x': [627.16, 625.39],
+ '142.y': [18.77, 14.13],
+ '143.height': [9.73, 7.33],
+ '143.width': [8.4, 6.32],
+ '143.x': [613.14, 614.84],
+ '143.y': [17.81, 13.41],
+ '144.height': [2.74, 2.06],
+ '144.width': [8.6, 6.48],
+ '144.x': [600.34, 605.2],
+ '144.y': [10.25, 7.71],
+ '145.height': [4.75, 3.57],
+ '145.width': [5.73, 4.32],
+ '145.x': [664.93, 653.83],
+ '145.y': [37.67, 28.36],
+ '146.height': [3.23, 2.43],
+ '146.width': [7.28, 5.48],
+ '146.x': [659.34, 649.62],
+ '146.y': [35.48, 26.71],
+ '147.height': [19.53, 14.7],
+ '147.width': [10.77, 8.11],
+ '147.x': [752.47, 719.72],
+ '147.y': [70.58, 53.13],
+ '148.height': [9.41, 7.08],
+ '148.width': [21.75, 16.38],
+ '148.x': [733.38, 705.35],
+ '148.y': [88.06, 66.29],
+ '149.height': [2.2, 1.66],
+ '149.width': [1.33, 1],
+ '149.x': [749.58, 717.54],
+ '149.y': [90.32, 67.99],
+ '150.height': [1.75, 1.31],
+ '150.width': [2.72, 2.04],
+ '150.x': [633.24, 629.97],
+ '150.y': [31.33, 23.58],
+ '151.height': [9.29, 6.99],
+ '151.width': [10.13, 7.63],
+ '151.x': [636.07, 632.1],
+ '151.y': [7.28, 5.48],
+ '152.height': [2.2, 1.66],
+ '152.width': [5.29, 3.98],
+ '152.x': [636.02, 632.06],
+ '152.y': [28.01, 21.09],
+ '153.height': [3.06, 2.31],
+ '153.width': [6.3, 4.74],
+ '153.x': [631.45, 628.62],
+ '153.y': [26.17, 19.7],
+ '154.height': [8.32, 6.27],
+ '154.width': [7.76, 5.84],
+ '154.x': [660.62, 650.57],
+ '154.y': [71.34, 53.7],
+ '155.height': [19.44, 14.63],
+ '155.width': [9.93, 7.48],
+ '155.x': [755.38, 721.91],
+ '155.y': [32.95, 24.8],
+ '156.height': [12, 9.03],
+ '156.width': [9.67, 7.28],
+ '156.x': [551.32, 568.3],
+ '156.y': [127.96, 96.33],
+ '157.height': [9.62, 7.24],
+ '157.width': [11.54, 8.68],
+ '157.x': [667.6, 655.83],
+ '157.y': [69.84, 52.58],
+ '158.height': [23, 17.32],
+ '158.width': [23.92, 18.01],
+ '158.x': [657.12, 647.94],
+ '158.y': [50.8, 38.24],
+ '159.height': [14.1, 10.61],
+ '159.width': [252.33, 189.95],
+ '159.x': [497.36, 527.68],
+ '159.y': [184.1, 138.59],
+ '160.height': [0.92, 0.69],
+ '160.width': [1.81, 1.36],
+ '160.x': [653.91, 645.53],
+ '160.y': [45.94, 34.58],
+ '161.height': [0.83, 0.62],
+ '161.width': [1.83, 1.38],
+ '161.x': [653.43, 645.16],
+ '161.y': [46.62, 35.1],
+ '162.height': [20.44, 15.39],
+ '162.width': [17.52, 13.19],
+ '162.x': [601.3, 605.92],
+ '162.y': [45.82, 34.49],
+ '163.height': [13.8, 10.39],
+ '163.width': [14.2, 10.69],
+ '163.x': [646.23, 639.74],
+ '163.y': [51.6, 38.85],
+ '164.height': [19.55, 14.71],
+ '164.width': [17.52, 13.19],
+ '164.x': [630.06, 627.57],
+ '164.y': [49.44, 37.21],
+ '165.height': [17.53, 13.2],
+ '165.width': [16.77, 12.62],
+ '165.x': [657.28, 648.06],
+ '165.y': [75.88, 57.12],
+ '166.height': [2.69, 2.02],
+ '166.width': [1.8, 1.35],
+ '166.x': [666.88, 655.29],
+ '166.y': [79.29, 59.69],
+ '167.height': [5.27, 3.96],
+ '167.width': [7.28, 5.48],
+ '167.x': [667.97, 656.11],
+ '167.y': [81.16, 61.1],
+ '168.height': [8.71, 6.56],
+ '168.width': [6.2, 4.66],
+ '168.x': [653.61, 645.3],
+ '168.y': [92.15, 69.36],
+ '169.height': [2.73, 2.05],
+ '169.width': [2.05, 1.54],
+ '169.x': [652.96, 644.81],
+ '169.y': [100.39, 75.57],
+ '170.height': [3.3, 2.48],
+ '170.width': [3.86, 2.91],
+ '170.x': [635.36, 631.56],
+ '170.y': [33.35, 25.1],
+ '171.height': [1.93, 1.45],
+ '171.width': [2.51, 1.89],
+ '171.x': [640.41, 635.37],
+ '171.y': [37.07, 27.91],
+ '172.height': [5, 3.76],
+ '172.width': [4.45, 3.35],
+ '172.x': [638.22, 633.71],
+ '172.y': [32.17, 24.22],
+ '173.height': [2.13, 1.6],
+ '173.width': [1.82, 1.37],
+ '173.x': [638.28, 633.76],
+ '173.y': [35.52, 26.74],
+ '174.height': [1.84, 1.39],
+ '174.width': [1.67, 1.26],
+ '174.x': [639.88, 634.96],
+ '174.y': [35.84, 26.98],
+ '175.height': [1.35, 1.02],
+ '175.width': [1.27, 0.96],
+ '175.x': [550.12, 567.4],
+ '175.y': [82.03, 61.75],
+ '176.height': [13.3, 10.02],
+ '176.width': [13.02, 9.8],
+ '176.x': [647, 640.32],
+ '176.y': [79.97, 60.2],
+ },
+ legend: {
+ 'continuousLegend.bounds.height': [46, 95],
+ 'continuousLegend.bar.height': [12, 15],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-maps.stories.tsx.usStatesCategoricalSpec @390': {
+ reason:
+ 'Approved B3 static map captions and neutral-class context reserve measured key space; occupied-bounds size-key placement has no legacy layout opt-out.',
+ changes: {
+ dims: {
+ 'mapSize.height': [265.55, 250.55],
+ },
+ area: {
+ 'rect.height': [265.55, 250.55],
+ 'rect.y': [90.85, 105.85],
+ },
+ marks: {
+ '0.y': [158.26, 150.76],
+ '1.y': [189.39, 181.89],
+ '2.y': [134.06, 126.56],
+ '3.y': [106.3, 98.8],
+ '4.y': [189.33, 181.83],
+ '5.y': [156.08, 148.58],
+ '6.y': [101.47, 93.97],
+ '7.y': [118.95, 111.45],
+ '8.y': [36.53, 29.03],
+ '9.y': [76.25, 68.75],
+ '10.y': [42.48, 34.98],
+ '11.y': [94.21, 86.71],
+ '12.y': [135.02, 127.52],
+ '13.y': [43.77, 36.27],
+ '14.y': [143.56, 136.06],
+ '15.y': [89.88, 82.38],
+ '16.y': [68.79, 61.29],
+ '17.y': [147.7, 140.2],
+ '18.y': [71.61, 64.11],
+ '19.y': [84.61, 77.11],
+ '20.y': [114.47, 106.97],
+ '21.y': [107.03, 99.53],
+ '22.y': [97.27, 89.77],
+ '23.y': [139.41, 131.91],
+ '24.y': [148.26, 140.76],
+ '25.y': [78.14, 70.64],
+ '26.y': [107.72, 100.22],
+ '27.y': [116.57, 109.07],
+ '28.y': [212.4, 204.9],
+ '29.y': [90.3, 82.8],
+ '30.y': [123.08, 115.58],
+ '31.y': [109.01, 101.51],
+ '32.y': [50.58, 43.08],
+ '33.y': [159.83, 152.33],
+ '34.y': [34.05, 26.55],
+ '35.y': [57.16, 49.66],
+ '36.y': [62.78, 55.28],
+ '37.y': [96.01, 88.51],
+ '38.y': [44.88, 37.38],
+ '39.y': [141.38, 133.88],
+ '40.y': [93.14, 85.64],
+ '41.y': [114.31, 106.81],
+ '42.y': [24.55, 17.05],
+ '43.y': [60.48, 52.98],
+ '44.y': [93.12, 85.62],
+ '45.y': [152.82, 145.32],
+ '46.y': [32.91, 25.41],
+ '47.y': [85.15, 77.65],
+ '48.y': [60.12, 52.62],
+ '49.y': [177.74, 170.24],
+ '50.y': [84, 76.5],
+ },
+ legend: {
+ 'categoricalLegend.bounds.height': [16, 31],
+ 'categoricalLegend.entry.0.y': [69.85, 84.85],
+ 'categoricalLegend.entry.1.y': [69.85, 84.85],
+ 'categoricalLegend.entry.2.y': [69.85, 84.85],
+ 'categoricalLegend.entry.3.y': [69.85, 84.85],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-maps.stories.tsx.usStatesCategoricalSpec @1280': {
+ reason:
+ 'Approved B3 static map captions and neutral-class context reserve measured key space; occupied-bounds size-key placement has no legacy layout opt-out.',
+ changes: {
+ dims: {
+ 'mapSize.height': [223.2, 208.2],
+ },
+ area: {
+ 'rect.height': [223.2, 208.2],
+ 'rect.y': [113.2, 128.2],
+ },
+ marks: {
+ '0.height': [42.48, 39.63],
+ '0.width': [26.04, 24.29],
+ '0.x': [692.13, 687.28],
+ '0.y': [137.88, 128.61],
+ '1.height': [52.3, 48.79],
+ '1.width': [97.97, 91.38],
+ '1.x': [429.23, 442.05],
+ '1.y': [169.98, 158.56],
+ '2.height': [53.75, 50.14],
+ '2.width': [45.99, 42.9],
+ '2.x': [504.42, 512.18],
+ '2.y': [112.93, 105.34],
+ '3.height': [38.93, 36.31],
+ '3.width': [49.14, 45.84],
+ '3.x': [550.41, 555.09],
+ '3.y': [84.3, 78.63],
+ '4.height': [53.28, 49.7],
+ '4.width': [61.55, 57.41],
+ '4.x': [699.17, 693.85],
+ '4.y': [169.92, 158.5],
+ '5.height': [38.49, 35.91],
+ '5.width': [36.74, 34.27],
+ '5.x': [710.07, 704.01],
+ '5.y': [135.63, 126.51],
+ '6.height': [36.19, 33.76],
+ '6.width': [20.95, 19.54],
+ '6.x': [690.8, 686.04],
+ '6.y': [79.32, 73.99],
+ '7.height': [27.12, 25.3],
+ '7.width': [50.08, 46.71],
+ '7.x': [597.42, 598.93],
+ '7.y': [97.35, 90.81],
+ '8.height': [39.22, 36.58],
+ '8.width': [24.88, 23.21],
+ '8.x': [785.89, 774.74],
+ '8.y': [12.35, 11.52],
+ '9.height': [12.96, 12.09],
+ '9.width': [23.46, 21.88],
+ '9.x': [778.27, 767.63],
+ '9.y': [53.31, 49.73],
+ '10.height': [50.05, 46.69],
+ '10.width': [44.47, 41.48],
+ '10.x': [631.14, 630.39],
+ '10.y': [18.49, 17.25],
+ '11.height': [20.73, 19.34],
+ '11.width': [9.02, 8.42],
+ '11.x': [770.21, 760.11],
+ '11.y': [71.83, 67.01],
+ '12.height': [26.41, 24.63],
+ '12.width': [59.65, 55.64],
+ '12.x': [718.86, 712.22],
+ '12.y': [113.91, 106.26],
+ '13.height': [27.9, 26.02],
+ '13.width': [44.44, 41.46],
+ '13.x': [590.44, 592.42],
+ '13.y': [19.82, 18.49],
+ '14.height': [30.78, 28.71],
+ '14.width': [58.34, 54.42],
+ '14.x': [590.64, 592.61],
+ '14.y': [122.72, 114.48],
+ '15.height': [25.47, 23.76],
+ '15.width': [39.29, 36.65],
+ '15.x': [735.22, 727.48],
+ '15.y': [67.37, 62.84],
+ '16.height': [31.64, 29.52],
+ '16.width': [47.2, 44.03],
+ '16.x': [588.26, 590.39],
+ '16.y': [45.62, 42.55],
+ '17.height': [92.85, 86.61],
+ '17.width': [94.69, 88.32],
+ '17.x': [561.57, 565.5],
+ '17.y': [127, 118.46],
+ '18.height': [39.3, 36.66],
+ '18.width': [47.2, 44.03],
+ '18.x': [542.48, 547.69],
+ '18.y': [48.53, 45.27],
+ '19.height': [11.52, 10.74],
+ '19.width': [11.68, 10.89],
+ '19.x': [778.47, 767.82],
+ '19.y': [61.93, 57.77],
+ '20.height': [39.25, 36.62],
+ '20.width': [45.05, 42.02],
+ '20.x': [639.67, 638.35],
+ '20.y': [92.72, 86.49],
+ '21.height': [30.43, 28.39],
+ '21.width': [30.36, 28.32],
+ '21.x': [726.59, 719.42],
+ '21.y': [85.05, 79.33],
+ '22.height': [48.01, 44.78],
+ '22.width': [26.94, 25.13],
+ '22.x': [667.16, 663.99],
+ '22.y': [74.98, 69.94],
+ '23.height': [49.04, 45.74],
+ '23.width': [47.25, 44.07],
+ '23.x': [543.71, 548.84],
+ '23.y': [118.44, 110.48],
+ '24.height': [31, 28.92],
+ '24.width': [34.1, 31.8],
+ '24.x': [647.55, 645.7],
+ '24.y': [127.57, 118.99],
+ '25.height': [92.93, 86.68],
+ '25.width': [54.25, 50.61],
+ '25.x': [457.85, 468.75],
+ '25.y': [55.26, 51.55],
+ '26.height': [11.83, 11.03],
+ '26.width': [7.09, 6.61],
+ '26.x': [768.58, 758.6],
+ '26.y': [85.76, 80],
+ '27.height': [1.72, 1.6],
+ '27.width': [1.43, 1.33],
+ '27.x': [761.45, 751.94],
+ '27.y': [94.89, 88.52],
+ '28.height': [28.2, 26.3],
+ '28.width': [43.57, 40.65],
+ '28.x': [532.2, 538.1],
+ '28.y': [193.71, 180.69],
+ '29.height': [26.86, 25.05],
+ '29.width': [40.81, 38.06],
+ '29.x': [634.22, 633.27],
+ '29.y': [67.8, 63.24],
+ '30.height': [25.7, 23.97],
+ '30.width': [50.06, 46.69],
+ '30.x': [681.77, 677.62],
+ '30.y': [101.6, 94.77],
+ '31.height': [15.29, 14.26],
+ '31.width': [30.62, 28.56],
+ '31.x': [745.05, 736.65],
+ '31.y': [87.09, 81.24],
+ '32.height': [53.84, 50.22],
+ '32.width': [51.22, 47.78],
+ '32.x': [670.99, 667.56],
+ '32.y': [26.84, 25.04],
+ '33.height': [41.92, 39.11],
+ '33.width': [24.17, 22.54],
+ '33.x': [669.82, 666.47],
+ '33.y': [139.5, 130.13],
+ '34.height': [43.87, 40.92],
+ '34.width': [69.03, 64.39],
+ '34.x': [523.57, 530.05],
+ '34.y': [9.8, 9.14],
+ '35.height': [23.48, 21.9],
+ '35.width': [11.13, 10.38],
+ '35.x': [782.01, 771.13],
+ '35.y': [33.62, 31.36],
+ '36.height': [38.95, 36.34],
+ '36.width': [50.97, 47.55],
+ '36.x': [739.5, 731.47],
+ '36.y': [39.42, 36.77],
+ '37.height': [32.36, 30.18],
+ '37.width': [28.31, 26.41],
+ '37.x': [708.78, 702.81],
+ '37.y': [73.69, 68.74],
+ '38.height': [45.88, 42.8],
+ '38.width': [54.37, 50.72],
+ '38.x': [462.58, 473.16],
+ '38.y': [20.96, 19.55],
+ '39.height': [20.07, 18.72],
+ '39.width': [57.58, 53.71],
+ '39.x': [677.58, 673.71],
+ '39.y': [120.47, 112.37],
+ '40.height': [47.72, 44.51],
+ '40.width': [38.01, 35.45],
+ '40.x': [517.09, 524.01],
+ '40.y': [70.72, 65.97],
+ '41.height': [29.93, 27.92],
+ '41.width': [53.67, 50.06],
+ '41.x': [721.54, 714.72],
+ '41.y': [92.56, 86.34],
+ '42.height': [33.61, 31.35],
+ '42.width': [45.35, 42.3],
+ '42.x': [474.58, 484.36],
+ '43.height': [39.06, 36.43],
+ '43.width': [36.29, 33.85],
+ '43.x': [656.73, 654.27],
+ '43.y': [37.05, 34.56],
+ '44.height': [27.91, 26.03],
+ '44.width': [55.84, 52.09],
+ '44.x': [586.8, 589.03],
+ '44.y': [70.71, 65.96],
+ '45.height': [26.26, 24.49],
+ '45.width': [34.42, 32.11],
+ '45.x': [725.85, 718.74],
+ '45.y': [132.27, 123.38],
+ '46.height': [65.3, 60.91],
+ '46.width': [40.29, 37.58],
+ '46.x': [506.88, 514.48],
+ '46.y': [8.62, 8.04],
+ '47.height': [66.56, 62.09],
+ '47.width': [43.04, 40.14],
+ '47.x': [482.22, 491.48],
+ '47.y': [62.49, 58.29],
+ '48.height': [21.48, 20.04],
+ '48.width': [11.41, 10.64],
+ '48.x': [773.36, 763.05],
+ '48.y': [36.68, 34.21],
+ '49.height': [34.03, 31.74],
+ '49.width': [39.1, 36.47],
+ '49.x': [652.08, 649.92],
+ '49.y': [157.97, 147.35],
+ '50.height': [7.51, 7.01],
+ '50.width': [5.26, 4.91],
+ '50.x': [788.86, 777.51],
+ '50.y': [61.3, 57.18],
+ },
+ legend: {
+ 'categoricalLegend.bounds.height': [16, 31],
+ 'categoricalLegend.entry.0.y': [92.2, 107.2],
+ 'categoricalLegend.entry.1.y': [92.2, 107.2],
+ 'categoricalLegend.entry.2.y': [92.2, 107.2],
+ 'categoricalLegend.entry.3.y': [92.2, 107.2],
+ },
+ },
+ style: {},
+ },
+};
diff --git a/examples/src/__tests__/migration-v8-attribution-changes.ts b/examples/src/__tests__/migration-v8-attribution-changes.ts
new file mode 100644
index 00000000..13102069
--- /dev/null
+++ b/examples/src/__tests__/migration-v8-attribution-changes.ts
@@ -0,0 +1,10117 @@
+import type { GeometryValue } from './parity-signature';
+
+/** C7 measured attribution, relative to the verified B3/P3 output.
+ * Exact released-v8/final-v9 pairs only; the v8 capture is never regenerated.
+ * A narrower measured brand reserve can enlarge the plot and change automatic
+ * label fitting. Every affected path remains guarded independently. */
+export const APPROVED_C7_ATTRIBUTION: Record<
+ string,
+ {
+ reason: string;
+ changes: Record>;
+ style: Record;
+ }
+> = {
+ '../charts/mobile-regression.stories.tsx.longTitleSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ area: {
+ 'rect.height': [106.3, 118.7],
+ },
+ chrome: {
+ bottomAnchorY: [332, 344.4],
+ bottomHeight: [52, 39.6],
+ 'source.maxWidth': [223, 292.58],
+ },
+ axes: {
+ 'x.end.y': [260.45, 272.85],
+ 'x.start.y': [260.45, 272.85],
+ 'y.end.y': [260.45, 272.85],
+ 'y.tick.0.position': [260.45, 272.85],
+ 'y.tick.1.position': [239.19, 249.11],
+ 'y.tick.2.position': [217.93, 225.37],
+ 'y.tick.3.position': [196.67, 201.63],
+ 'y.tick.4.position': [175.41, 177.89],
+ 'y.titlePosition.y': [207.3, 213.5],
+ },
+ marks: {
+ '0.height': [17.11, 19.11],
+ '0.y': [243.34, 253.74],
+ '1.height': [44.11, 49.26],
+ '1.y': [216.34, 223.59],
+ '2.height': [78.13, 87.25],
+ '2.y': [182.32, 185.61],
+ '3.height': [89.29, 99.71],
+ '3.y': [171.16, 173.14],
+ '4.height': [96.41, 107.66],
+ '4.y': [164.04, 165.19],
+ },
+ labels: {
+ '0.label.y': [231.34, 241.74],
+ '1.label.y': [204.34, 211.59],
+ '2.label.y': [170.32, 173.61],
+ '3.label.y': [159.16, 161.14],
+ '4.label.y': [152.04, 153.19],
+ },
+ },
+ style: {},
+ },
+ '../charts/mobile-regression.stories.tsx.longTitleSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../charts/mobile-regression.stories.tsx.groupedLabelsSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../charts/mobile-regression.stories.tsx.groupedLabelsSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../charts/mobile-regression.stories.tsx.manyRowsSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../charts/mobile-regression.stories.tsx.manyRowsSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../charts/mobile-regression.stories.tsx.domainScoresSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../charts/mobile-regression.stories.tsx.domainScoresSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../charts/mobile-regression.stories.tsx.oneWideLabelSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../charts/mobile-regression.stories.tsx.oneWideLabelSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../charts/mobile-regression.stories.tsx.oneWideLabelThemedSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../charts/mobile-regression.stories.tsx.oneWideLabelThemedSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../charts/mobile-regression.stories.tsx.percentBucketsSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../charts/mobile-regression.stories.tsx.percentBucketsSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../charts/mobile-regression.stories.tsx.inlineYTitleSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../charts/mobile-regression.stories.tsx.inlineYTitleSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../charts/mobile-regression.stories.tsx.autoHeightChromeGrowthSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../charts/mobile-regression.stories.tsx.chromeGrowSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.height': [600.6, 588.2],
+ },
+ chrome: {
+ bottomHeight: [52, 39.6],
+ grownHeight: [200.6, 188.2],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../charts/mobile-regression.stories.tsx.chromeGrowSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../charts/mobile-regression.stories.tsx.chromeMaxLinesSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ area: {
+ 'rect.height': [195.7, 208.1],
+ },
+ chrome: {
+ bottomAnchorY: [332, 344.4],
+ bottomHeight: [52, 39.6],
+ 'source.maxWidth': [223, 292.58],
+ },
+ axes: {
+ 'x.end.y': [306, 318.4],
+ 'x.start.y': [306, 318.4],
+ 'y.end.y': [306, 318.4],
+ 'y.tick.0.position': [306, 318.4],
+ 'y.tick.1.position': [266.86, 276.78],
+ 'y.tick.2.position': [227.72, 235.16],
+ 'y.tick.3.position': [188.58, 193.54],
+ 'y.tick.4.position': [149.44, 151.92],
+ 'y.titlePosition.y': [208.15, 214.35],
+ },
+ marks: {
+ '0.height': [31.51, 33.5],
+ '0.y': [274.49, 284.9],
+ '1.height': [81.22, 86.36],
+ '1.y': [224.78, 232.04],
+ '2.height': [143.84, 152.95],
+ '2.y': [162.16, 165.45],
+ '3.height': [164.39, 174.8],
+ '3.y': [141.61, 143.6],
+ '4.height': [177.5, 188.75],
+ '4.y': [128.5, 129.65],
+ },
+ labels: {
+ '0.label.y': [262.49, 272.9],
+ '1.label.y': [212.78, 220.04],
+ '2.label.y': [150.16, 153.45],
+ '3.label.y': [129.61, 131.6],
+ '4.label.y': [116.5, 117.65],
+ },
+ },
+ style: {},
+ },
+ '../charts/mobile-regression.stories.tsx.chromeMaxLinesSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-bar-column.stories.tsx.simpleBarsSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-bar-column.stories.tsx.simpleBarsSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-bar-column.stories.tsx.columnsSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-bar-column.stories.tsx.columnsSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-bar-column.stories.tsx.groupedColumnsSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-bar-column.stories.tsx.groupedColumnsSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-bar-column.stories.tsx.stackedBarsSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-bar-column.stories.tsx.stackedBarsSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-bar-column.stories.tsx.normalizedBarsSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-bar-column.stories.tsx.normalizedBarsSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-bar-column.stories.tsx.divergingColumnsSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-bar-column.stories.tsx.divergingColumnsSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-bar-column.stories.tsx.negativeValuesSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-bar-column.stories.tsx.negativeValuesSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-bar-column.stories.tsx.barListSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [215, 284.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-bar-column.stories.tsx.barListSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-bar-column.stories.tsx.longLabelsSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-bar-column.stories.tsx.longLabelsSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-bar-column.stories.tsx.interactiveSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-bar-column.stories.tsx.interactiveSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-building-blocks.stories.tsx.directLabelSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ area: {
+ 'rect.height': [136.3, 150.3],
+ },
+ chrome: {
+ bottomAnchorY: [312, 326],
+ bottomHeight: [72, 58],
+ 'byline.maxWidth': [223, 292.58],
+ 'byline.y': [42, 28],
+ 'source.maxWidth': [223, 292.58],
+ },
+ axes: {
+ 'x.end.y': [264, 278],
+ 'x.start.y': [264, 278],
+ 'x.titlePosition.y': [299, 313],
+ 'y.end.y': [264, 278],
+ 'y.tick.0.position': [246.07, 258.22],
+ 'y.tick.1.position': [210.2, 218.67],
+ 'y.tick.2.position': [174.33, 179.12],
+ 'y.tick.3.position': [138.46, 139.57],
+ 'y.titlePosition.y': [195.85, 202.85],
+ },
+ marks: {
+ '0.cy': [142.05, 143.52],
+ '1.cy': [172.54, 177.14],
+ '10.y': [128.05, 129.52],
+ '11.y': [158.54, 163.14],
+ '12.y': [197.63, 206.25],
+ '13.y': [186.87, 194.39],
+ '14.y': [223.1, 234.34],
+ '2.cy': [211.63, 220.25],
+ '3.cy': [200.87, 208.39],
+ '4.cy': [237.1, 248.34],
+ '5.cy': [235.66, 246.75],
+ '6.cy': [239.61, 251.1],
+ '7.cy': [242.48, 254.27],
+ '8.cy': [248.58, 260.99],
+ '9.cy': [253.96, 266.93],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-building-blocks.stories.tsx.directLabelSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-building-blocks.stories.tsx.ruleMarkSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ area: {
+ 'rect.height': [138.3, 152.3],
+ },
+ chrome: {
+ bottomAnchorY: [286, 300],
+ bottomHeight: [72, 58],
+ 'byline.maxWidth': [223, 292.58],
+ 'byline.y': [42, 28],
+ 'source.maxWidth': [223, 292.58],
+ },
+ axes: {
+ 'y.end.y': [286, 300],
+ 'y.tick.0.position': [286, 300],
+ 'y.tick.1.position': [260.85, 272.31],
+ 'y.tick.2.position': [235.71, 244.62],
+ 'y.tick.3.position': [210.56, 216.93],
+ 'y.tick.4.position': [185.42, 189.24],
+ 'y.tick.5.position': [160.27, 161.55],
+ 'y.titlePosition.y': [216.85, 223.85],
+ },
+ marks: {
+ '0.y1': [151.97, 152.41],
+ '0.y2': [151.97, 152.41],
+ '1.y1': [179.13, 182.31],
+ '1.y2': [179.13, 182.31],
+ '2.y1': [197.99, 203.08],
+ '2.y2': [197.99, 203.08],
+ '3.y1': [235.71, 244.62],
+ '3.y2': [235.71, 244.62],
+ '4.y1': [286, 300],
+ '4.y2': [286, 300],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-building-blocks.stories.tsx.ruleMarkSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-building-blocks.stories.tsx.tickMarkSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-building-blocks.stories.tsx.tickMarkSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-building-blocks.stories.tsx.layerSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-building-blocks.stories.tsx.layerSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-building-blocks.stories.tsx.dualAxisSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-building-blocks.stories.tsx.dualAxisSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-building-blocks.stories.tsx.temporalDualAxisSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-building-blocks.stories.tsx.temporalDualAxisSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-building-blocks.stories.tsx.spanSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ area: {
+ 'rect.height': [143.68, 172.3],
+ },
+ chrome: {
+ bottomAnchorY: [297.38, 326],
+ bottomHeight: [72, 58],
+ 'byline.maxWidth': [223, 292.58],
+ 'byline.y': [42, 28],
+ 'source.maxWidth': [223, 292.58],
+ },
+ axes: {
+ 'x.end.y': [271.38, 300],
+ 'x.start.y': [271.38, 300],
+ 'y.end.y': [271.38, 300],
+ 'y.tick.0.position': [271.38, 300],
+ 'y.tick.1.position': [241.45, 264.1],
+ 'y.tick.2.position': [211.51, 228.21],
+ 'y.tick.3.position': [181.58, 192.31],
+ 'y.tick.4.position': [151.65, 156.42],
+ 'y.titlePosition.y': [199.54, 213.85],
+ },
+ marks: {
+ '0.y1': [250.43, 274.87],
+ '0.y2': [215.71, 233.23],
+ '1.y1': [265.99, 293.54],
+ '1.y2': [216.3, 233.95],
+ '10.y1': [244.44, 267.69],
+ '10.y2': [200.74, 215.29],
+ '11.y1': [246.24, 269.85],
+ '11.y2': [212.71, 229.64],
+ '2.y1': [252.22, 277.03],
+ '2.y2': [206.73, 222.47],
+ '3.y1': [215.11, 232.52],
+ '3.y2': [157.63, 163.6],
+ '4.y1': [199.54, 213.85],
+ '4.y2': [139.07, 141.34],
+ '5.y1': [170.8, 179.39],
+ '5.y2': [119.32, 117.65],
+ '6.y1': [139.07, 141.34],
+ '6.y2': [90.58, 83.19],
+ '7.y1': [150.45, 154.98],
+ '7.y2': [106.15, 101.85],
+ '8.y1': [167.81, 175.8],
+ '8.y2': [125.9, 125.55],
+ '9.y1': [197.15, 210.98],
+ '9.y2': [155.84, 161.44],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-building-blocks.stories.tsx.spanSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-building-blocks.stories.tsx.composedSpecForPanel @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ area: {
+ 'rect.height': [173.7, 186.1],
+ },
+ chrome: {
+ bottomAnchorY: [332, 344.4],
+ bottomHeight: [52, 39.6],
+ 'source.maxWidth': [223, 292.58],
+ },
+ axes: {
+ 'x.end.y': [284, 296.4],
+ 'x.start.y': [284, 296.4],
+ 'x.titlePosition.y': [319, 331.4],
+ 'y.end.y': [284, 296.4],
+ 'y.tick.0.position': [261.14, 271.91],
+ 'y.tick.1.position': [215.43, 222.94],
+ 'y.tick.2.position': [169.72, 173.97],
+ 'y.tick.3.position': [124.01, 124.99],
+ 'y.titlePosition.y': [197.15, 203.35],
+ },
+ marks: {
+ '0.cy': [128.58, 129.89],
+ '1.cy': [167.44, 171.52],
+ '10.y': [114.58, 115.89],
+ '11.y': [153.44, 157.52],
+ '12.y': [203.26, 210.9],
+ '13.y': [189.55, 196.21],
+ '14.y': [235.72, 245.67],
+ '15.y': [233.89, 243.71],
+ '16.y': [238.92, 249.1],
+ '17.y': [242.57, 253.02],
+ '18.y': [250.34, 261.34],
+ '19.y': [257.2, 268.69],
+ '2.cy': [217.26, 224.9],
+ '3.cy': [203.55, 210.21],
+ '4.cy': [249.72, 259.67],
+ '5.cy': [247.89, 257.71],
+ '6.cy': [252.92, 263.1],
+ '7.cy': [256.57, 267.02],
+ '8.cy': [264.34, 275.34],
+ '9.cy': [271.2, 282.69],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-building-blocks.stories.tsx.composedSpecForPanel @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-line-area.stories.tsx.singleLineSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-line-area.stories.tsx.singleLineSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-line-area.stories.tsx.multiSeriesSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ area: {
+ 'rect.height': [138.7, 152.7],
+ },
+ chrome: {
+ bottomAnchorY: [312, 326],
+ bottomHeight: [72, 58],
+ 'byline.maxWidth': [223, 292.58],
+ 'byline.y': [42, 28],
+ 'source.maxWidth': [223, 292.58],
+ },
+ axes: {
+ 'x.end.y': [286, 300],
+ 'x.start.y': [286, 300],
+ 'y.end.y': [286, 300],
+ 'y.tick.0.position': [268.66, 280.91],
+ 'y.tick.1.position': [225.32, 233.19],
+ 'y.tick.2.position': [181.98, 185.48],
+ 'y.titlePosition.y': [216.65, 223.65],
+ },
+ marks: {
+ '0.height': [71.95, 79.21],
+ '0.y': [172.44, 174.98],
+ '1.height': [104.02, 114.52],
+ '1.y': [174.17, 176.89],
+ '2.height': [53.75, 59.17],
+ '2.y': [152.5, 153.03],
+ },
+ annotations: {
+ 'a0.line.end.y': [225.32, 233.19],
+ 'a0.line.start.y': [225.32, 233.19],
+ 'a1.rect.height': [138.7, 152.7],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-line-area.stories.tsx.multiSeriesSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-line-area.stories.tsx.fiveSeriesSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ area: {
+ 'rect.height': [138.7, 152.7],
+ },
+ chrome: {
+ bottomAnchorY: [312, 326],
+ bottomHeight: [72, 58],
+ 'byline.maxWidth': [223, 292.58],
+ 'byline.y': [42, 28],
+ 'source.maxWidth': [223, 292.58],
+ },
+ axes: {
+ 'x.end.y': [286, 300],
+ 'x.start.y': [286, 300],
+ 'y.end.y': [286, 300],
+ 'y.tick.0.position': [252.52, 263.14],
+ 'y.tick.1.position': [204.69, 210.49],
+ 'y.tick.2.position': [156.87, 157.83],
+ 'y.titlePosition.y': [216.65, 223.65],
+ },
+ marks: {
+ '0.height': [85.61, 94.25],
+ '0.y': [147.78, 147.83],
+ '1.height': [30.61, 33.7],
+ '1.y': [205.65, 211.54],
+ '2.height': [45.2, 49.76],
+ '2.y': [216.65, 223.65],
+ '3.height': [30.37, 33.44],
+ '3.y': [237.69, 246.82],
+ '4.height': [22.24, 24.48],
+ '4.y': [261.13, 272.62],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-line-area.stories.tsx.fiveSeriesSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-line-area.stories.tsx.singleAreaSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-line-area.stories.tsx.singleAreaSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-line-area.stories.tsx.stackedAreaSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-line-area.stories.tsx.stackedAreaSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-line-area.stories.tsx.timeAxisSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-line-area.stories.tsx.timeAxisSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-line-area.stories.tsx.logScaleSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-line-area.stories.tsx.logScaleSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-line-area.stories.tsx.interactiveSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ area: {
+ 'rect.height': [161.3, 173.7],
+ },
+ chrome: {
+ bottomAnchorY: [332, 344.4],
+ bottomHeight: [52, 39.6],
+ 'source.maxWidth': [223, 292.58],
+ },
+ axes: {
+ 'x.end.y': [306, 318.4],
+ 'x.start.y': [306, 318.4],
+ 'y.end.y': [306, 318.4],
+ 'y.tick.0.position': [267.07, 276.47],
+ 'y.tick.1.position': [211.44, 216.58],
+ 'y.tick.2.position': [155.82, 156.68],
+ 'y.titlePosition.y': [225.35, 231.55],
+ },
+ marks: {
+ '0.height': [99.56, 107.21],
+ '0.y': [145.26, 145.3],
+ '1.height': [35.6, 38.33],
+ '1.y': [212.56, 217.77],
+ '2.height': [52.56, 56.6],
+ '2.y': [225.35, 231.55],
+ '3.height': [35.32, 38.03],
+ '3.y': [249.82, 257.9],
+ '4.height': [25.86, 27.85],
+ '4.y': [277.08, 287.25],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-line-area.stories.tsx.interactiveSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-line-area.stories.tsx.seriesSearchSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-line-area.stories.tsx.seriesSearchSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-pie-donut.stories.tsx.basicPieSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-pie-donut.stories.tsx.basicPieSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-pie-donut.stories.tsx.donutSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-pie-donut.stories.tsx.donutSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-pie-donut.stories.tsx.smallSliceSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-pie-donut.stories.tsx.smallSliceSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-pie-donut.stories.tsx.manyCategoriesSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-pie-donut.stories.tsx.manyCategoriesSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-pie-donut.stories.tsx.leaderLineSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ area: {
+ 'rect.height': [201.7, 215.7],
+ },
+ chrome: {
+ bottomAnchorY: [312, 326],
+ bottomHeight: [72, 58],
+ 'byline.maxWidth': [223, 292.58],
+ 'byline.y': [42, 28],
+ 'source.maxWidth': [223, 292.58],
+ },
+ marks: {
+ '0.cy': [211.15, 218.15],
+ '0.innerRadius': [51.43, 55],
+ '0.outerRadius': [85.72, 91.67],
+ '1.cy': [211.15, 218.15],
+ '1.innerRadius': [51.43, 55],
+ '1.outerRadius': [85.72, 91.67],
+ '2.cy': [211.15, 218.15],
+ '2.innerRadius': [51.43, 55],
+ '2.outerRadius': [85.72, 91.67],
+ '3.cy': [211.15, 218.15],
+ '3.innerRadius': [51.43, 55],
+ '3.outerRadius': [85.72, 91.67],
+ '4.cy': [211.15, 218.15],
+ '4.innerRadius': [51.43, 55],
+ '4.outerRadius': [85.72, 91.67],
+ '5.cy': [211.15, 218.15],
+ '5.innerRadius': [51.43, 55],
+ '5.outerRadius': [85.72, 91.67],
+ '6.cy': [211.15, 218.15],
+ '6.innerRadius': [51.43, 55],
+ '6.outerRadius': [85.72, 91.67],
+ },
+ labels: {
+ '0.label.x': [259.4, 263.32],
+ '0.label.y': [131.05, 133.58],
+ '1.label.x': [278.76, 283.86],
+ '1.label.y': [254.89, 264.95],
+ '2.label.x': [107.88, 107.02],
+ '2.label.y': [301.24, 314.13],
+ '3.label.x': [48.62, 43.35],
+ '3.label.y': [249.98, 259.74],
+ '4.label.x': [31.51, 25.74],
+ '4.label.y': [181.22, 186.8],
+ '5.label.x': [77.91, 73.74],
+ '5.label.y': [134.81, 137.57],
+ '6.label.x': [118.73, 117.24],
+ '6.label.y': [109.95, 111.19],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-pie-donut.stories.tsx.leaderLineSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-pie-donut.stories.tsx.interactiveSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-pie-donut.stories.tsx.interactiveSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-pie-donut.stories.tsx.waffleSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-pie-donut.stories.tsx.waffleSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-pie-donut.stories.tsx.parliamentSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-pie-donut.stories.tsx.parliamentSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-pie-donut.stories.tsx.halfDonutSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-pie-donut.stories.tsx.halfDonutSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-scatter-distribution.stories.tsx.basicScatterSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ area: {
+ 'rect.height': [62.95, 76.95],
+ },
+ chrome: {
+ bottomAnchorY: [265.1, 279.1],
+ bottomHeight: [72, 58],
+ 'byline.maxWidth': [223, 292.58],
+ 'byline.y': [42, 28],
+ 'source.maxWidth': [223, 292.58],
+ },
+ axes: {
+ 'x.end.y': [217.1, 231.1],
+ 'x.start.y': [217.1, 231.1],
+ 'x.titlePosition.y': [252.1, 266.1],
+ 'y.end.y': [217.1, 231.1],
+ 'y.tick.0.position': [210.8, 223.41],
+ 'y.tick.1.position': [195.07, 204.17],
+ 'y.tick.2.position': [179.33, 184.93],
+ 'y.tick.3.position': [163.59, 165.69],
+ 'y.titlePosition.y': [185.63, 192.63],
+ },
+ marks: {
+ '0.height': [34.3, 41.92],
+ '0.y': [170.03, 173.56],
+ '1.cy': [155.72, 156.07],
+ '10.cy': [182.79, 189.16],
+ '11.cy': [182.79, 189.16],
+ '12.cy': [183.42, 189.93],
+ '13.cy': [184.37, 191.09],
+ '14.cy': [187.2, 194.55],
+ '15.cy': [187.51, 194.93],
+ '16.cy': [188.46, 196.09],
+ '17.cy': [189.4, 197.24],
+ '18.cy': [190.35, 198.4],
+ '19.cy': [192.55, 201.09],
+ '2.cy': [168, 171.08],
+ '20.cy': [207.03, 218.79],
+ '21.cy': [212.38, 225.33],
+ '22.cy': [216.16, 229.95],
+ '3.cy': [170.83, 174.54],
+ '4.cy': [176.18, 181.08],
+ '5.cy': [176.81, 181.85],
+ '6.cy': [180.27, 186.08],
+ '7.cy': [181.53, 187.62],
+ '8.cy': [181.85, 188.01],
+ '9.cy': [182.79, 189.16],
+ },
+ annotations: {
+ 'a0.connector.from.x': [279.93, 279.91],
+ 'a0.connector.from.y': [155.3, 157.88],
+ 'a0.connector.to.x': [295.93, 295.95],
+ 'a0.connector.to.y': [155.64, 156.43],
+ 'a0.label.y': [158.15, 164.15],
+ 'a1.connector.from.x': [318.05, 317.66],
+ 'a1.connector.from.y': [196.76, 204.61],
+ 'a1.connector.to.x': [299.43, 299.44],
+ 'a1.connector.to.y': [191.44, 199.48],
+ 'a1.label.y': [210.4, 218.24],
+ 'a2.connector.from.y': [174.95, 179.85],
+ 'a2.connector.to.y': [175.82, 180.72],
+ 'a2.label.y': [176.18, 181.08],
+ 'a3.label.y': [184.14, 191.7],
+ 'a3.line.end.y': [188.14, 195.7],
+ 'a3.line.start.y': [188.14, 195.7],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-scatter-distribution.stories.tsx.basicScatterSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-scatter-distribution.stories.tsx.bubbleSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-scatter-distribution.stories.tsx.bubbleSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-scatter-distribution.stories.tsx.colorScatterSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-scatter-distribution.stories.tsx.colorScatterSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-scatter-distribution.stories.tsx.trendScatterSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-scatter-distribution.stories.tsx.dotPlotSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ area: {
+ 'rect.height': [136.3, 150.3],
+ },
+ chrome: {
+ bottomAnchorY: [312, 326],
+ bottomHeight: [72, 58],
+ 'byline.maxWidth': [223, 292.58],
+ 'byline.y': [42, 28],
+ 'source.maxWidth': [223, 292.58],
+ },
+ axes: {
+ 'x.end.y': [264, 278],
+ 'x.start.y': [264, 278],
+ 'x.titlePosition.y': [299, 313],
+ 'y.end.y': [264, 278],
+ 'y.tick.0.position': [257.05, 270.33],
+ 'y.tick.1.position': [245.92, 258.06],
+ 'y.tick.10.position': [145.78, 147.64],
+ 'y.tick.11.position': [134.65, 135.37],
+ 'y.tick.2.position': [234.79, 245.79],
+ 'y.tick.3.position': [223.67, 233.52],
+ 'y.tick.4.position': [212.54, 221.25],
+ 'y.tick.5.position': [201.41, 208.98],
+ 'y.tick.6.position': [190.29, 196.72],
+ 'y.tick.7.position': [179.16, 184.45],
+ 'y.tick.8.position': [168.03, 172.18],
+ 'y.tick.9.position': [156.91, 159.91],
+ },
+ marks: {
+ '0.y': [256.05, 269.33],
+ '1.cy': [257.05, 270.33],
+ '10.y': [200.41, 207.98],
+ '11.cy': [201.41, 208.98],
+ '12.y': [189.29, 195.72],
+ '13.cy': [190.29, 196.72],
+ '14.y': [178.16, 183.45],
+ '15.cy': [179.16, 184.45],
+ '16.y': [167.03, 171.18],
+ '17.cy': [168.03, 172.18],
+ '18.y': [155.91, 158.91],
+ '19.cy': [156.91, 159.91],
+ '2.y': [244.92, 257.06],
+ '20.y': [144.78, 146.64],
+ '21.cy': [145.78, 147.64],
+ '22.y': [133.65, 134.37],
+ '23.cy': [134.65, 135.37],
+ '3.cy': [245.92, 258.06],
+ '4.y': [233.79, 244.79],
+ '5.cy': [234.79, 245.79],
+ '6.y': [222.67, 232.52],
+ '7.cy': [223.67, 233.52],
+ '8.y': [211.54, 220.25],
+ '9.cy': [212.54, 221.25],
+ },
+ labels: {
+ '1.label.y': [250.45, 263.73],
+ '11.label.y': [194.81, 202.38],
+ '13.label.y': [167.85, 174.28],
+ '15.label.y': [172.56, 177.85],
+ '17.label.y': [145.59, 149.74],
+ '19.label.y': [150.31, 153.31],
+ '21.label.y': [123.34, 125.2],
+ '23.label.y': [128.05, 128.77],
+ '3.label.y': [239.32, 251.46],
+ '5.label.y': [212.35, 223.35],
+ '7.label.y': [217.07, 226.92],
+ '9.label.y': [190.1, 198.81],
+ },
+ annotations: {
+ 'a0.label.y': [274.05, 287.33],
+ 'a0.line.end.y': [264, 278],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-scatter-distribution.stories.tsx.dotPlotSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-scatter-distribution.stories.tsx.lollipopSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ area: {
+ 'rect.height': [162.75, 176.75],
+ },
+ chrome: {
+ bottomAnchorY: [312, 326],
+ bottomHeight: [72, 58],
+ 'byline.maxWidth': [223, 292.58],
+ 'byline.y': [42, 28],
+ 'source.maxWidth': [223, 292.58],
+ },
+ axes: {
+ 'x.end.y': [264, 278],
+ 'x.start.y': [264, 278],
+ 'x.titlePosition.y': [299, 313],
+ 'y.end.y': [264, 278],
+ 'y.tick.0.position': [256.32, 269.66],
+ 'y.tick.1.position': [244.04, 256.32],
+ 'y.tick.10.position': [133.49, 136.27],
+ 'y.tick.11.position': [121.21, 122.93],
+ 'y.tick.12.position': [108.93, 109.59],
+ 'y.tick.2.position': [231.76, 242.98],
+ 'y.tick.3.position': [219.47, 229.64],
+ 'y.tick.4.position': [207.19, 216.3],
+ 'y.tick.5.position': [194.91, 202.96],
+ 'y.tick.6.position': [182.63, 189.62],
+ 'y.tick.7.position': [170.34, 176.29],
+ 'y.tick.8.position': [158.06, 162.95],
+ 'y.tick.9.position': [145.78, 149.61],
+ },
+ marks: {
+ '0.cy': [256.32, 269.66],
+ '1.cy': [244.04, 256.32],
+ '10.cy': [133.49, 136.27],
+ '11.cy': [121.21, 122.93],
+ '12.cy': [108.93, 109.59],
+ '2.cy': [231.76, 242.98],
+ '3.cy': [219.47, 229.64],
+ '4.cy': [207.19, 216.3],
+ '5.cy': [194.91, 202.96],
+ '6.cy': [182.63, 189.62],
+ '7.cy': [170.34, 176.29],
+ '8.cy': [158.06, 162.95],
+ '9.cy': [145.78, 149.61],
+ },
+ labels: {
+ '0.label.y': [249.72, 263.06],
+ '1.label.y': [237.44, 249.72],
+ '10.label.x': [113.05, 126.85],
+ '10.label.y': [126.89, 129.67],
+ '11.label.y': [98.77, 116.33],
+ '12.label.x': [147.26, 115.8],
+ '12.label.y': [102.33, 102.99],
+ '2.label.y': [209.32, 236.38],
+ '3.label.y': [212.87, 223.04],
+ '4.label.y': [184.75, 209.7],
+ '5.label.y': [204.15, 196.36],
+ '6.label.y': [176.03, 183.02],
+ '7.label.y': [147.9, 169.69],
+ '8.label.y': [151.46, 156.35],
+ '9.label.y': [123.34, 143.01],
+ },
+ annotations: {
+ 'a0.line.end.y': [264, 278],
+ },
+ },
+ style: {
+ 'marks.12.label.connector': [undefined, null],
+ 'marks.12.label.connector.arrow': [false, undefined],
+ 'marks.12.label.connector.stroke': ['#d1495b', undefined],
+ 'marks.12.label.connector.style': ['straight', undefined],
+ },
+ },
+ '../gallery/charts-scatter-distribution.stories.tsx.lollipopSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-scatter-distribution.stories.tsx.dumbbellSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ area: {
+ 'rect.height': [89.85, 103.85],
+ },
+ chrome: {
+ bottomAnchorY: [312, 326],
+ bottomHeight: [72, 58],
+ 'byline.maxWidth': [223, 292.58],
+ 'byline.y': [42, 28],
+ 'source.maxWidth': [223, 292.58],
+ },
+ axes: {
+ 'x.end.y': [264, 278],
+ 'x.start.y': [264, 278],
+ 'x.titlePosition.y': [299, 313],
+ 'y.end.y': [264, 278],
+ 'y.tick.0.position': [259.42, 272.7],
+ 'y.tick.1.position': [252.08, 264.22],
+ 'y.tick.10.position': [186.07, 187.93],
+ 'y.tick.11.position': [178.73, 179.45],
+ 'y.tick.2.position': [244.75, 255.75],
+ 'y.tick.3.position': [237.41, 247.27],
+ 'y.tick.4.position': [230.08, 238.79],
+ 'y.tick.5.position': [222.74, 230.31],
+ 'y.tick.6.position': [215.41, 221.84],
+ 'y.tick.7.position': [208.07, 213.36],
+ 'y.tick.8.position': [200.74, 204.88],
+ 'y.tick.9.position': [193.4, 196.4],
+ },
+ marks: {
+ '0.y': [258.42, 271.7],
+ '1.cy': [259.42, 272.7],
+ '10.cy': [237.41, 247.27],
+ '11.cy': [237.41, 247.27],
+ '12.y': [229.08, 237.79],
+ '13.cy': [230.08, 238.79],
+ '14.cy': [230.08, 238.79],
+ '15.y': [221.74, 229.31],
+ '16.cy': [222.74, 230.31],
+ '17.cy': [222.74, 230.31],
+ '18.y': [214.41, 220.84],
+ '19.cy': [215.41, 221.84],
+ '2.cy': [259.42, 272.7],
+ '20.cy': [215.41, 221.84],
+ '21.y': [207.07, 212.36],
+ '22.cy': [208.07, 213.36],
+ '23.cy': [208.07, 213.36],
+ '24.y': [199.74, 203.88],
+ '25.cy': [200.74, 204.88],
+ '26.cy': [200.74, 204.88],
+ '27.y': [192.4, 195.4],
+ '28.cy': [193.4, 196.4],
+ '29.cy': [193.4, 196.4],
+ '3.y': [251.08, 263.22],
+ '30.y': [185.07, 186.93],
+ '31.cy': [186.07, 187.93],
+ '32.cy': [186.07, 187.93],
+ '33.y': [177.73, 178.45],
+ '34.cy': [178.73, 179.45],
+ '35.cy': [178.73, 179.45],
+ '4.cy': [252.08, 264.22],
+ '5.cy': [252.08, 264.22],
+ '6.y': [243.75, 254.75],
+ '7.cy': [244.75, 255.75],
+ '8.cy': [244.75, 255.75],
+ '9.y': [236.41, 246.27],
+ },
+ labels: {
+ '1.label.y': [252.82, 266.1],
+ '10.label.y': [214.97, 224.83],
+ '11.label.y': [214.97, 224.83],
+ '13.label.y': [223.48, 232.19],
+ '14.label.y': [207.64, 216.35],
+ '16.label.y': [200.3, 207.87],
+ '17.label.y': [200.3, 207.87],
+ '19.label.y': [208.81, 215.24],
+ '2.label.y': [252.82, 266.1],
+ '20.label.y': [192.97, 199.4],
+ '22.label.y': [201.47, 206.76],
+ '23.label.y': [185.63, 190.92],
+ '25.label.y': [178.3, 182.44],
+ '26.label.y': [178.3, 182.44],
+ '28.label.y': [186.8, 189.8],
+ '29.label.y': [186.8, 189.8],
+ '31.label.y': [179.47, 181.33],
+ '32.label.y': [163.63, 165.49],
+ '34.label.y': [156.29, 157.01],
+ '35.label.y': [172.13, 172.85],
+ '4.label.y': [229.64, 241.78],
+ '5.label.y': [245.48, 257.62],
+ '7.label.y': [238.15, 249.15],
+ '8.label.y': [222.31, 233.31],
+ },
+ annotations: {
+ 'a0.connector.from.y': [180.32, 181.04],
+ 'a0.connector.to.y': [178.94, 179.65],
+ 'a0.dot.y': [178.73, 179.45],
+ 'a0.label.y': [178.73, 179.45],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-scatter-distribution.stories.tsx.dumbbellSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-scatter-distribution.stories.tsx.stripSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-scatter-distribution.stories.tsx.stripSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-scatter-distribution.stories.tsx.rangeDumbbellSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-scatter-distribution.stories.tsx.rangeDumbbellSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-scatter-distribution.stories.tsx.arrowPlotSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-scatter-distribution.stories.tsx.arrowPlotSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-scatter-distribution.stories.tsx.rangeBarSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ area: {
+ 'rect.height': [143.68, 157.68],
+ },
+ chrome: {
+ bottomAnchorY: [312, 326],
+ bottomHeight: [72, 58],
+ 'byline.maxWidth': [223, 292.58],
+ 'byline.y': [42, 28],
+ 'source.maxWidth': [223, 292.58],
+ },
+ axes: {
+ 'x.end.y': [271.38, 285.38],
+ 'x.start.y': [271.38, 285.38],
+ 'y.end.y': [271.38, 285.38],
+ 'y.tick.0.position': [271.38, 285.38],
+ 'y.tick.1.position': [226.48, 236.11],
+ 'y.tick.2.position': [181.58, 186.83],
+ 'y.tick.3.position': [136.68, 137.56],
+ 'y.titlePosition.y': [199.54, 206.54],
+ },
+ marks: {
+ '0.height': [26.04, 28.58],
+ '0.y': [229.62, 239.56],
+ '1.height': [37.27, 40.9],
+ '1.y': [230.07, 240.05],
+ '10.height': [32.78, 35.97],
+ '10.y': [218.4, 227.24],
+ '11.height': [25.14, 27.59],
+ '11.y': [227.38, 237.09],
+ '2.height': [34.12, 37.45],
+ '2.y': [222.89, 232.16],
+ '3.height': [43.1, 47.3],
+ '3.y': [186.07, 191.76],
+ '4.height': [45.35, 49.77],
+ '4.y': [172.15, 176.48],
+ '5.height': [38.61, 42.38],
+ '5.y': [157.33, 160.22],
+ '6.height': [36.37, 39.91],
+ '6.y': [135.78, 136.57],
+ '7.height': [33.23, 36.46],
+ '7.y': [147.46, 149.38],
+ '8.height': [31.43, 34.49],
+ '8.y': [162.27, 165.64],
+ '9.height': [30.98, 34],
+ '9.y': [184.72, 190.28],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-scatter-distribution.stories.tsx.rangeBarSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-scatter-distribution.stories.tsx.interactiveSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-scatter-distribution.stories.tsx.interactiveSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-scatter-distribution.stories.tsx.beeswarmSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-scatter-distribution.stories.tsx.beeswarmSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-scatter-distribution.stories.tsx.calendarSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-scatter-distribution.stories.tsx.calendarSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-scatter-distribution.stories.tsx.heatmapSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ area: {
+ 'rect.height': [129.3, 143.3],
+ },
+ chrome: {
+ bottomAnchorY: [312, 326],
+ bottomHeight: [72, 58],
+ 'byline.maxWidth': [223, 292.58],
+ 'byline.y': [42, 28],
+ 'source.maxWidth': [223, 292.58],
+ },
+ axes: {
+ 'x.end.y': [286, 300],
+ 'x.start.y': [286, 300],
+ 'y.end.y': [286, 300],
+ 'y.tick.0.position': [274.87, 287.66],
+ 'y.tick.1.position': [253.46, 263.94],
+ 'y.tick.2.position': [232.05, 240.21],
+ 'y.tick.3.position': [210.65, 216.49],
+ 'y.tick.4.position': [189.24, 192.76],
+ 'y.tick.5.position': [167.83, 169.04],
+ },
+ marks: {
+ '0.height': [20.55, 22.78],
+ '0.y': [264.59, 276.27],
+ '1.height': [20.55, 22.78],
+ '1.y': [243.19, 252.55],
+ '10.height': [20.55, 22.78],
+ '10.y': [178.96, 181.37],
+ '11.height': [20.55, 22.78],
+ '11.y': [157.56, 157.65],
+ '12.height': [20.55, 22.78],
+ '12.y': [264.59, 276.27],
+ '13.height': [20.55, 22.78],
+ '13.y': [243.19, 252.55],
+ '14.height': [20.55, 22.78],
+ '14.y': [221.78, 228.82],
+ '15.height': [20.55, 22.78],
+ '15.y': [200.37, 205.1],
+ '16.height': [20.55, 22.78],
+ '16.y': [178.96, 181.37],
+ '17.height': [20.55, 22.78],
+ '17.y': [157.56, 157.65],
+ '18.height': [20.55, 22.78],
+ '18.y': [264.59, 276.27],
+ '19.height': [20.55, 22.78],
+ '19.y': [243.19, 252.55],
+ '2.height': [20.55, 22.78],
+ '2.y': [221.78, 228.82],
+ '20.height': [20.55, 22.78],
+ '20.y': [221.78, 228.82],
+ '21.height': [20.55, 22.78],
+ '21.y': [200.37, 205.1],
+ '22.height': [20.55, 22.78],
+ '22.y': [178.96, 181.37],
+ '23.height': [20.55, 22.78],
+ '23.y': [157.56, 157.65],
+ '24.height': [20.55, 22.78],
+ '24.y': [264.59, 276.27],
+ '25.height': [20.55, 22.78],
+ '25.y': [243.19, 252.55],
+ '26.height': [20.55, 22.78],
+ '26.y': [221.78, 228.82],
+ '27.height': [20.55, 22.78],
+ '27.y': [200.37, 205.1],
+ '28.height': [20.55, 22.78],
+ '28.y': [178.96, 181.37],
+ '29.height': [20.55, 22.78],
+ '29.y': [157.56, 157.65],
+ '3.height': [20.55, 22.78],
+ '3.y': [200.37, 205.1],
+ '30.height': [20.55, 22.78],
+ '30.y': [264.59, 276.27],
+ '31.height': [20.55, 22.78],
+ '31.y': [243.19, 252.55],
+ '32.height': [20.55, 22.78],
+ '32.y': [221.78, 228.82],
+ '33.height': [20.55, 22.78],
+ '33.y': [200.37, 205.1],
+ '34.height': [20.55, 22.78],
+ '34.y': [178.96, 181.37],
+ '35.height': [20.55, 22.78],
+ '35.y': [157.56, 157.65],
+ '36.height': [20.55, 22.78],
+ '36.y': [264.59, 276.27],
+ '37.height': [20.55, 22.78],
+ '37.y': [243.19, 252.55],
+ '38.height': [20.55, 22.78],
+ '38.y': [221.78, 228.82],
+ '39.height': [20.55, 22.78],
+ '39.y': [200.37, 205.1],
+ '4.height': [20.55, 22.78],
+ '4.y': [178.96, 181.37],
+ '40.height': [20.55, 22.78],
+ '40.y': [178.96, 181.37],
+ '41.height': [20.55, 22.78],
+ '41.y': [157.56, 157.65],
+ '42.height': [20.55, 22.78],
+ '42.y': [264.59, 276.27],
+ '43.height': [20.55, 22.78],
+ '43.y': [243.19, 252.55],
+ '44.height': [20.55, 22.78],
+ '44.y': [221.78, 228.82],
+ '45.height': [20.55, 22.78],
+ '45.y': [200.37, 205.1],
+ '46.height': [20.55, 22.78],
+ '46.y': [178.96, 181.37],
+ '47.height': [20.55, 22.78],
+ '47.y': [157.56, 157.65],
+ '5.height': [20.55, 22.78],
+ '5.y': [157.56, 157.65],
+ '6.height': [20.55, 22.78],
+ '6.y': [264.59, 276.27],
+ '7.height': [20.55, 22.78],
+ '7.y': [243.19, 252.55],
+ '8.height': [20.55, 22.78],
+ '8.y': [221.78, 228.82],
+ '9.height': [20.55, 22.78],
+ '9.y': [200.37, 205.1],
+ },
+ },
+ style: {},
+ },
+ '../gallery/charts-scatter-distribution.stories.tsx.heatmapSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/dashboards.layouts.tsx.saasMrrSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/dashboards.layouts.tsx.saasMrrSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/dashboards.layouts.tsx.saasAccountsSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 390],
+ },
+ },
+ style: {},
+ },
+ '../gallery/dashboards.layouts.tsx.saasAccountsSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 1280],
+ },
+ },
+ style: {},
+ },
+ '../gallery/dashboards.layouts.tsx.opsErrorSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/dashboards.layouts.tsx.opsErrorSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/dashboards.layouts.tsx.marketsCompareSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/dashboards.layouts.tsx.marketsCompareSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/dashboards.layouts.tsx.funnelSankeySpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [215, 284.58],
+ 'source.maxWidth': [215, 284.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/dashboards.layouts.tsx.funnelSankeySpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1105, 1174.58],
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/dashboards.layouts.tsx.incidentErrorSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/dashboards.layouts.tsx.incidentErrorSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/dashboards.layouts.tsx.incidentActivitySpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 390],
+ },
+ },
+ style: {},
+ },
+ '../gallery/dashboards.layouts.tsx.incidentActivitySpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 1280],
+ },
+ },
+ style: {},
+ },
+ '../gallery/dashboards.stories.tsx.kpiSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/dashboards.stories.tsx.kpiSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/dashboards.stories.tsx.moversSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [215, 284.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/dashboards.stories.tsx.moversSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/dashboards.stories.tsx.crosshairSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/dashboards.stories.tsx.crosshairSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/dashboards.stories.tsx.sectorSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/dashboards.stories.tsx.sectorSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-animation.stories.tsx.pieSweepSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ area: {
+ 'rect.height': [230.75, 243.15],
+ },
+ chrome: {
+ bottomAnchorY: [332, 344.4],
+ bottomHeight: [52, 39.6],
+ 'source.maxWidth': [223, 292.58],
+ },
+ marks: {
+ '0.cy': [216.63, 222.83],
+ '0.outerRadius': [98.07, 103.34],
+ '1.cy': [216.63, 222.83],
+ '1.outerRadius': [98.07, 103.34],
+ '2.cy': [216.63, 222.83],
+ '2.outerRadius': [98.07, 103.34],
+ '3.cy': [216.63, 222.83],
+ '3.outerRadius': [98.07, 103.34],
+ '4.cy': [216.63, 222.83],
+ '4.outerRadius': [98.07, 103.34],
+ '5.cy': [216.63, 222.83],
+ '5.outerRadius': [98.07, 103.34],
+ '6.cy': [216.63, 222.83],
+ '6.outerRadius': [98.07, 103.34],
+ },
+ labels: {
+ '0.label.x': [267.54, 271.01],
+ '0.label.y': [127.24, 129.48],
+ '1.label.x': [289.34, 293.86],
+ '1.label.y': [266.72, 275.64],
+ '2.label.x': [106.1, 105.33],
+ '2.label.y': [318.93, 330.35],
+ '3.label.x': [37.69, 33.02],
+ '3.label.y': [261.19, 269.84],
+ '4.label.x': [19.53, 14.41],
+ '4.label.y': [183.75, 188.69],
+ '5.label.x': [69.26, 65.57],
+ '5.label.y': [131.48, 133.91],
+ '6.label.x': [115.63, 114.31],
+ '6.label.y': [103.47, 104.57],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-animation.stories.tsx.pieSweepSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-animation.stories.tsx.donutSweepSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ area: {
+ 'rect.height': [230.75, 243.15],
+ },
+ chrome: {
+ bottomAnchorY: [332, 344.4],
+ bottomHeight: [52, 39.6],
+ 'source.maxWidth': [223, 292.58],
+ },
+ marks: {
+ '0.cy': [216.63, 222.83],
+ '0.innerRadius': [58.84, 62],
+ '0.outerRadius': [98.07, 103.34],
+ '1.cy': [216.63, 222.83],
+ '1.innerRadius': [58.84, 62],
+ '1.outerRadius': [98.07, 103.34],
+ '2.cy': [216.63, 222.83],
+ '2.innerRadius': [58.84, 62],
+ '2.outerRadius': [98.07, 103.34],
+ '3.cy': [216.63, 222.83],
+ '3.innerRadius': [58.84, 62],
+ '3.outerRadius': [98.07, 103.34],
+ '4.cy': [216.63, 222.83],
+ '4.innerRadius': [58.84, 62],
+ '4.outerRadius': [98.07, 103.34],
+ '5.cy': [216.63, 222.83],
+ '5.innerRadius': [58.84, 62],
+ '5.outerRadius': [98.07, 103.34],
+ '6.cy': [216.63, 222.83],
+ '6.innerRadius': [58.84, 62],
+ '6.outerRadius': [98.07, 103.34],
+ },
+ labels: {
+ '0.label.x': [267.54, 271.01],
+ '0.label.y': [127.24, 129.48],
+ '1.label.x': [289.34, 293.86],
+ '1.label.y': [266.72, 275.64],
+ '2.label.x': [106.1, 105.33],
+ '2.label.y': [318.93, 330.35],
+ '3.label.x': [37.69, 33.02],
+ '3.label.y': [261.19, 269.84],
+ '4.label.x': [19.53, 14.41],
+ '4.label.y': [183.75, 188.69],
+ '5.label.x': [69.26, 65.57],
+ '5.label.y': [131.48, 133.91],
+ '6.label.x': [115.63, 114.31],
+ '6.label.y': [103.47, 104.57],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-animation.stories.tsx.donutSweepSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-animation.stories.tsx.stackedChainSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-animation.stories.tsx.stackedChainSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-animation.stories.tsx.annotationDelaySpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-animation.stories.tsx.annotationDelaySpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-animation.stories.tsx.updateSpecForPanel @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-animation.stories.tsx.updateSpecForPanel @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-animation.stories.tsx.enterExitSpecForPanel @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-animation.stories.tsx.enterExitSpecForPanel @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-annotations.stories.tsx.textAnnotationSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-annotations.stories.tsx.textAnnotationSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-annotations.stories.tsx.connectorsSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-annotations.stories.tsx.connectorsSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-annotations.stories.tsx.richTextSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-annotations.stories.tsx.richTextSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-annotations.stories.tsx.ledeSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-annotations.stories.tsx.ledeSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-annotations.stories.tsx.rangeSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-annotations.stories.tsx.rangeSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-annotations.stories.tsx.rectangleRangeSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-annotations.stories.tsx.rectangleRangeSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-annotations.stories.tsx.reflineSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-annotations.stories.tsx.reflineSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-annotations.stories.tsx.stabilitySpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-annotations.stories.tsx.stabilitySpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-annotations.stories.tsx.thinningSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-annotations.stories.tsx.thinningSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-annotations.stories.tsx.thinningDisabledSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-annotations.stories.tsx.thinningDisabledSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-data-encoding.stories.tsx.filterBaseSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-data-encoding.stories.tsx.filterBaseSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-data-encoding.stories.tsx.filterOffSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-data-encoding.stories.tsx.filterOffSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-data-encoding.stories.tsx.binSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-data-encoding.stories.tsx.binSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-data-encoding.stories.tsx.calculateSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-data-encoding.stories.tsx.calculateSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-data-encoding.stories.tsx.timeUnitSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-data-encoding.stories.tsx.timeUnitSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-data-encoding.stories.tsx.conditionalSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-data-encoding.stories.tsx.conditionalSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-data-encoding.stories.tsx.linearGradientSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-data-encoding.stories.tsx.linearGradientSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-data-encoding.stories.tsx.areaGradientSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-data-encoding.stories.tsx.areaGradientSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-data-encoding.stories.tsx.facetSharedSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.height': [714.7, 700.7],
+ },
+ chrome: {
+ bottomHeight: [72, 58],
+ 'byline.maxWidth': [223, 292.58],
+ 'byline.y': [42, 28],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-data-encoding.stories.tsx.facetSharedSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-data-encoding.stories.tsx.facetIndependentSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.height': [714.7, 700.7],
+ },
+ chrome: {
+ bottomHeight: [72, 58],
+ 'byline.maxWidth': [223, 292.58],
+ 'byline.y': [42, 28],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-data-encoding.stories.tsx.facetIndependentSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-data-encoding.stories.tsx.rowFacetSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-data-encoding.stories.tsx.rowFacetSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-data-encoding.stories.tsx.formatSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-data-encoding.stories.tsx.formatSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-data-encoding.stories.tsx.fillPatternSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-data-encoding.stories.tsx.fillPatternSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-data-encoding.stories.tsx.dashEncodingSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ area: {
+ 'rect.height': [141.3, 155.3],
+ },
+ chrome: {
+ bottomAnchorY: [312, 326],
+ bottomHeight: [72, 58],
+ 'byline.maxWidth': [223, 292.58],
+ 'byline.y': [42, 28],
+ 'source.maxWidth': [223, 292.58],
+ },
+ axes: {
+ 'x.end.y': [286, 300],
+ 'x.start.y': [286, 300],
+ 'y.end.y': [286, 300],
+ 'y.tick.0.position': [268.34, 280.59],
+ 'y.tick.1.position': [224.18, 232.06],
+ 'y.tick.2.position': [180.02, 183.52],
+ 'y.titlePosition.y': [215.35, 222.35],
+ },
+ marks: {
+ '0.height': [73.3, 80.56],
+ '0.y': [170.31, 172.85],
+ '1.height': [105.98, 116.48],
+ '1.y': [172.08, 174.79],
+ '2.height': [54.75, 60.18],
+ '2.y': [150, 150.52],
+ },
+ endpointLabels: {
+ '0.labelY': [undefined, 199.27],
+ '1.labelY': [undefined, 221.52],
+ '2.labelY': [undefined, 177.02],
+ 'bounds.height': [undefined, 155.3],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-data-encoding.stories.tsx.dashEncodingSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-responsive.stories.tsx.flagshipSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ area: {
+ 'rect.height': [147.75, 161.75],
+ },
+ chrome: {
+ bottomAnchorY: [312, 326],
+ bottomHeight: [72, 58],
+ 'byline.maxWidth': [223, 292.58],
+ 'byline.y': [42, 28],
+ 'source.maxWidth': [223, 292.58],
+ },
+ axes: {
+ 'x.end.y': [286, 300],
+ 'x.start.y': [286, 300],
+ 'y.end.y': [286, 300],
+ 'y.tick.0.position': [267.53, 279.78],
+ 'y.tick.1.position': [221.36, 229.23],
+ 'y.tick.2.position': [175.19, 178.69],
+ 'y.titlePosition.y': [212.13, 219.13],
+ },
+ marks: {
+ '0.height': [76.65, 83.91],
+ '0.y': [165.03, 167.57],
+ '1.height': [110.81, 121.31],
+ '1.y': [166.88, 169.59],
+ '2.height': [57.25, 62.68],
+ '2.y': [143.79, 144.32],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-responsive.stories.tsx.flagshipSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-responsive.stories.tsx.autoHeightSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-responsive.stories.tsx.autoHeightSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-responsive.stories.tsx.rotationSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-responsive.stories.tsx.rotationSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-responsive.stories.tsx.ratioSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ area: {
+ 'rect.height': [208.55, 220.95],
+ },
+ chrome: {
+ bottomAnchorY: [332, 344.4],
+ bottomHeight: [52, 39.6],
+ 'source.maxWidth': [223, 292.58],
+ },
+ axes: {
+ 'x.end.y': [306, 318.4],
+ 'x.start.y': [306, 318.4],
+ 'y.end.y': [306, 318.4],
+ 'y.gridlines': [3, 8],
+ 'y.tick.0.label': ['−5', '−6'],
+ 'y.tick.0.position': [279.93, 304.59],
+ 'y.tick.1.label': ['0', '−4'],
+ 'y.tick.1.position': [214.76, 276.97],
+ 'y.tick.2.label': ['5', '−2'],
+ 'y.tick.2.position': [149.59, 249.35],
+ 'y.tick.3.label': [undefined, '0'],
+ 'y.tick.3.position': [undefined, 221.73],
+ 'y.tick.4.label': [undefined, '2'],
+ 'y.tick.4.position': [undefined, 194.12],
+ 'y.tick.5.label': [undefined, '4'],
+ 'y.tick.5.position': [undefined, 166.5],
+ 'y.tick.6.label': [undefined, '6'],
+ 'y.tick.6.position': [undefined, 138.88],
+ 'y.tick.7.label': [undefined, '8'],
+ 'y.tick.7.position': [undefined, 111.26],
+ },
+ marks: {
+ '0.height': [108.19, 114.62],
+ '0.y': [135.25, 137.5],
+ '1.height': [156.41, 165.71],
+ '1.y': [137.86, 140.26],
+ '2.height': [80.81, 85.62],
+ '2.y': [105.27, 105.74],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-responsive.stories.tsx.ratioSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-responsive.stories.tsx.facetSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-responsive.stories.tsx.facetSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1105, 1174.58],
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-theming.stories.tsx.lineSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-theming.stories.tsx.lineSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-theming.stories.tsx.barSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-theming.stories.tsx.barSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-theming.stories.tsx.presetSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-theming.stories.tsx.presetSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-theming.stories.tsx.darkModeSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/features-theming.stories.tsx.darkModeSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/maps.stories.tsx.usStateSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [235, 304.58],
+ 'source.maxWidth': [235, 304.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/maps.stories.tsx.usStateSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1105, 1174.58],
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/maps.stories.tsx.usCountySpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [235, 304.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/maps.stories.tsx.usCountySpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/maps.stories.tsx.worldEqualEarthSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'mapSize.height': [166.7, 142.7],
+ },
+ area: {
+ 'rect.height': [166.7, 142.7],
+ },
+ chrome: {
+ bottomAnchorY: [324, 338],
+ bottomHeight: [66, 52],
+ 'byline.maxWidth': [235, 304.58],
+ 'byline.y': [42, 28],
+ 'source.maxWidth': [235, 304.58],
+ },
+ marks: {
+ '0.height': [2.85, 2.44],
+ '0.width': [337.62, 289.01],
+ '0.x': [16.19, 40.49],
+ '0.y': [103.46, 88.56],
+ '1.height': [13.81, 11.82],
+ '1.width': [10.23, 8.76],
+ '1.x': [213, 208.97],
+ '1.y': [84.19, 72.07],
+ '10.height': [42.81, 36.65],
+ '10.width': [15.01, 12.85],
+ '10.x': [119.38, 128.83],
+ '10.y': [105.42, 90.24],
+ '100.height': [1.91, 1.63],
+ '100.width': [3.07, 2.63],
+ '100.x': [265.33, 253.76],
+ '100.y': [47.32, 40.51],
+ '101.height': [4.84, 4.14],
+ '101.width': [7.95, 6.81],
+ '101.x': [256.98, 246.62],
+ '101.y': [44.78, 38.33],
+ '102.height': [15.96, 13.66],
+ '102.width': [13.24, 11.33],
+ '102.x': [239.46, 231.62],
+ '102.y': [36.96, 31.64],
+ '103.height': [10.67, 9.13],
+ '103.width': [11.62, 9.94],
+ '103.x': [238.15, 230.5],
+ '103.y': [35.43, 30.33],
+ '104.height': [4.74, 4.06],
+ '104.width': [7.08, 6.06],
+ '104.x': [242.43, 234.16],
+ '104.y': [32.67, 27.97],
+ '105.height': [4.42, 3.78],
+ '105.width': [8.08, 6.91],
+ '105.x': [243.88, 235.4],
+ '105.y': [30.12, 25.78],
+ '106.height': [8.39, 7.18],
+ '106.width': [13.28, 11.37],
+ '106.x': [228.97, 222.64],
+ '106.y': [30.71, 26.29],
+ '107.height': [17.17, 14.7],
+ '107.width': [19.9, 17.03],
+ '107.x': [222.5, 217.1],
+ '107.y': [34.06, 29.15],
+ '108.height': [5.69, 4.87],
+ '108.width': [5.23, 4.48],
+ '108.x': [216.24, 211.74],
+ '108.y': [36.85, 31.55],
+ '109.height': [2.79, 2.39],
+ '109.width': [3.03, 2.59],
+ '109.x': [221.66, 216.38],
+ '109.y': [32.36, 27.7],
+ '11.height': [23.75, 20.33],
+ '11.width': [18.17, 15.55],
+ '11.x': [196.61, 194.94],
+ '11.y': [76.21, 65.24],
+ '110.height': [11.13, 9.52],
+ '110.width': [8.14, 6.97],
+ '110.x': [193.03, 191.87],
+ '110.y': [6.81, 5.83],
+ '111.height': [4.62, 3.96],
+ '111.width': [7.15, 6.12],
+ '111.x': [202.91, 200.33],
+ '111.y': [17.2, 14.72],
+ '112.height': [7.16, 6.13],
+ '112.width': [14.27, 12.21],
+ '112.x': [202.63, 200.09],
+ '112.y': [20.82, 17.82],
+ '113.height': [5.69, 4.87],
+ '113.width': [8.05, 6.89],
+ '113.x': [195.78, 194.23],
+ '113.y': [18.42, 15.77],
+ '114.height': [2.69, 2.3],
+ '114.width': [5.94, 5.09],
+ '114.x': [192.65, 191.55],
+ '114.y': [24.1, 20.63],
+ '115.height': [2.97, 2.54],
+ '115.width': [5.12, 4.38],
+ '115.x': [198.09, 196.21],
+ '115.y': [24.52, 20.99],
+ '116.height': [3.1, 2.65],
+ '116.width': [3.05, 2.61],
+ '116.x': [206.29, 203.22],
+ '116.y': [24.68, 21.13],
+ '117.height': [4.77, 4.08],
+ '117.width': [7.83, 6.71],
+ '117.x': [201.43, 199.06],
+ '117.y': [24.93, 21.34],
+ '118.height': [2.3, 1.97],
+ '118.width': [4.3, 3.68],
+ '118.x': [200.75, 198.48],
+ '118.y': [17.01, 14.56],
+ '119.height': [2.14, 1.83],
+ '119.width': [5.38, 4.61],
+ '119.x': [200.67, 198.41],
+ '119.y': [15.57, 13.33],
+ '12.height': [17.59, 15.06],
+ '12.width': [9.24, 7.91],
+ '12.x': [224.14, 218.51],
+ '12.y': [67.55, 57.82],
+ '120.height': [1.88, 1.61],
+ '120.width': [3.46, 2.96],
+ '120.x': [201.94, 199.5],
+ '120.y': [14.13, 12.1],
+ '121.height': [7.59, 6.49],
+ '121.width': [7.08, 6.06],
+ '121.x': [189.65, 188.98],
+ '121.y': [18.29, 15.66],
+ '122.height': [3.26, 2.79],
+ '122.width': [5.12, 4.38],
+ '122.x': [203.49, 200.83],
+ '122.y': [29.11, 24.92],
+ '123.height': [7.79, 6.66],
+ '123.width': [5.78, 4.95],
+ '123.x': [202.11, 199.65],
+ '123.y': [31.72, 27.15],
+ '124.height': [7.09, 6.07],
+ '124.width': [16.71, 14.3],
+ '124.x': [206.86, 203.72],
+ '124.y': [31.38, 26.86],
+ '125.height': [3.38, 2.89],
+ '125.width': [1.6, 1.37],
+ '125.x': [201.12, 198.8],
+ '125.y': [30.78, 26.35],
+ '126.height': [4.3, 3.68],
+ '126.width': [4.69, 4.02],
+ '126.x': [196.16, 194.56],
+ '126.y': [26.71, 22.87],
+ '127.height': [2.14, 1.83],
+ '127.width': [3.55, 3.04],
+ '127.x': [189.88, 189.18],
+ '127.y': [25.33, 21.69],
+ '128.height': [0.69, 0.59],
+ '128.width': [0.44, 0.37],
+ '128.x': [189.49, 188.84],
+ '128.y': [23, 19.69],
+ '129.height': [1.94, 1.66],
+ '129.width': [2.86, 2.45],
+ '129.x': [186.96, 186.68],
+ '129.y': [21.66, 18.54],
+ '13.height': [13.1, 11.22],
+ '13.width': [7.57, 6.48],
+ '13.x': [217.39, 212.73],
+ '13.y': [75.89, 64.96],
+ '130.height': [2.64, 2.26],
+ '130.width': [2.86, 2.45],
+ '130.x': [187.58, 187.21],
+ '130.y': [19.69, 16.85],
+ '131.height': [6.08, 5.2],
+ '131.width': [2.77, 2.37],
+ '131.x': [176.87, 178.04],
+ '131.y': [31.22, 26.73],
+ '132.height': [8.69, 7.44],
+ '132.width': [10.35, 8.86],
+ '132.x': [177.2, 178.32],
+ '132.y': [29.63, 25.37],
+ '133.height': [3.32, 2.84],
+ '133.width': [3.12, 2.67],
+ '133.x': [177.25, 178.37],
+ '133.y': [18.16, 15.54],
+ '134.height': [2.85, 2.44],
+ '134.width': [1.9, 1.62],
+ '134.x': [337.15, 315.25],
+ '134.y': [108.58, 92.95],
+ '135.height': [5.41, 4.63],
+ '135.width': [4.82, 4.13],
+ '135.x': [334.06, 312.6],
+ '135.y': [91.46, 78.29],
+ '136.height': [13.48, 11.54],
+ '136.width': [18.02, 15.43],
+ '136.x': [320.31, 300.83],
+ '136.y': [125.89, 107.77],
+ '137.height': [39.52, 33.83],
+ '137.width': [38.62, 33.06],
+ '137.x': [285.74, 271.23],
+ '137.y': [96.66, 82.75],
+ '138.height': [4.94, 4.23],
+ '138.width': [2.05, 1.75],
+ '138.x': [260.79, 249.88],
+ '138.y': [70.35, 60.22],
+ '139.height': [40.01, 34.25],
+ '139.width': [47.06, 40.28],
+ '139.x': [247.57, 238.56],
+ '139.y': [19.74, 16.9],
+ '14.height': [17.38, 14.88],
+ '14.width': [15.12, 12.95],
+ '14.x': [205.72, 202.74],
+ '14.y': [55.01, 47.09],
+ '140.height': [4.09, 3.5],
+ '140.width': [1.28, 1.09],
+ '140.x': [295.19, 279.33],
+ '140.y': [50.96, 43.62],
+ '141.height': [11.47, 9.82],
+ '141.width': [10.11, 8.66],
+ '141.x': [190.53, 189.73],
+ '141.y': [26.07, 22.32],
+ '142.height': [2.68, 2.3],
+ '142.width': [3.5, 3],
+ '142.x': [191.02, 190.16],
+ '142.y': [15.78, 13.51],
+ '143.height': [8.19, 7.01],
+ '143.width': [7.06, 6.05],
+ '143.x': [179.23, 180.06],
+ '143.y': [14.98, 12.83],
+ '144.height': [2.3, 1.97],
+ '144.width': [7.24, 6.19],
+ '144.x': [168.47, 170.85],
+ '144.y': [8.62, 7.38],
+ '145.height': [3.99, 3.42],
+ '145.width': [4.82, 4.13],
+ '145.x': [222.79, 217.35],
+ '145.y': [31.68, 27.12],
+ '146.height': [2.71, 2.32],
+ '146.width': [6.12, 5.24],
+ '146.x': [218.09, 213.33],
+ '146.y': [29.84, 25.55],
+ '147.height': [16.43, 14.06],
+ '147.width': [9.06, 7.76],
+ '147.x': [296.42, 280.38],
+ '147.y': [59.36, 50.82],
+ '148.height': [7.91, 6.78],
+ '148.width': [18.3, 15.66],
+ '148.x': [280.36, 266.63],
+ '148.y': [74.06, 63.4],
+ '149.height': [1.85, 1.58],
+ '149.width': [1.12, 0.95],
+ '149.x': [293.99, 278.29],
+ '149.y': [75.96, 65.03],
+ '15.height': [20.16, 17.25],
+ '15.width': [9.68, 8.29],
+ '15.x': [197.75, 195.91],
+ '15.y': [53.27, 45.6],
+ '150.height': [1.47, 1.26],
+ '150.width': [2.28, 1.96],
+ '150.x': [196.14, 194.53],
+ '150.y': [26.35, 22.55],
+ '151.height': [7.81, 6.69],
+ '151.width': [8.52, 7.3],
+ '151.x': [198.52, 196.57],
+ '151.y': [6.12, 5.24],
+ '152.height': [1.85, 1.58],
+ '152.width': [4.45, 3.81],
+ '152.x': [198.47, 196.53],
+ '152.y': [23.56, 20.17],
+ '153.height': [2.58, 2.21],
+ '153.width': [5.3, 4.53],
+ '153.x': [194.63, 193.24],
+ '153.y': [22.01, 18.85],
+ '154.height': [7, 5.99],
+ '154.width': [6.53, 5.59],
+ '154.x': [219.16, 214.24],
+ '154.y': [60, 51.36],
+ '155.height': [16.35, 13.99],
+ '155.width': [8.35, 7.15],
+ '155.x': [298.86, 282.47],
+ '155.y': [27.71, 23.72],
+ '156.height': [10.09, 8.64],
+ '156.width': [8.14, 6.97],
+ '156.x': [127.23, 135.55],
+ '156.y': [107.63, 92.13],
+ '157.height': [8.09, 6.92],
+ '157.width': [9.7, 8.31],
+ '157.x': [225.03, 219.27],
+ '157.y': [58.74, 50.29],
+ '158.height': [19.35, 16.56],
+ '158.width': [20.12, 17.22],
+ '158.x': [216.22, 211.73],
+ '158.y': [42.72, 36.57],
+ '159.height': [11.86, 10.15],
+ '159.width': [212.22, 181.67],
+ '159.x': [81.85, 96.7],
+ '159.y': [154.84, 132.55],
+ '16.height': [2.36, 2.02],
+ '16.width': [2.82, 2.41],
+ '16.x': [115.57, 125.57],
+ '16.y': [57.6, 49.31],
+ '160.height': [0.77, 0.66],
+ '160.width': [1.52, 1.3],
+ '160.x': [213.52, 209.42],
+ '160.y': [38.64, 33.08],
+ '161.height': [0.7, 0.6],
+ '161.width': [1.54, 1.32],
+ '161.x': [213.12, 209.07],
+ '161.y': [39.21, 33.57],
+ '162.height': [17.19, 14.72],
+ '162.width': [14.73, 12.61],
+ '162.x': [169.27, 171.54],
+ '162.y': [38.54, 32.99],
+ '163.height': [11.61, 9.94],
+ '163.width': [11.94, 10.22],
+ '163.x': [207.06, 203.88],
+ '163.y': [43.4, 37.15],
+ '164.height': [16.44, 14.07],
+ '164.width': [14.74, 12.62],
+ '164.x': [193.46, 192.24],
+ '164.y': [41.58, 35.59],
+ '165.height': [14.74, 12.62],
+ '165.width': [14.1, 12.07],
+ '165.x': [216.35, 211.84],
+ '165.y': [63.82, 54.64],
+ '166.height': [2.26, 1.93],
+ '166.width': [1.51, 1.29],
+ '166.x': [224.43, 218.75],
+ '166.y': [66.69, 57.09],
+ '167.height': [4.43, 3.79],
+ '167.width': [6.12, 5.24],
+ '167.x': [225.34, 219.53],
+ '167.y': [68.26, 58.44],
+ '168.height': [7.33, 6.27],
+ '168.width': [5.21, 4.46],
+ '168.x': [213.27, 209.2],
+ '168.y': [77.5, 66.34],
+ '169.height': [2.29, 1.96],
+ '169.width': [1.72, 1.47],
+ '169.x': [212.72, 208.73],
+ '169.y': [84.43, 72.28],
+ '17.height': [2.86, 2.45],
+ '17.width': [3.38, 2.89],
+ '17.x': [117.97, 127.62],
+ '17.y': [57.64, 49.34],
+ '170.height': [2.77, 2.38],
+ '170.width': [3.25, 2.78],
+ '170.x': [197.92, 196.06],
+ '170.y': [28.05, 24.01],
+ '171.height': [1.62, 1.39],
+ '171.width': [2.11, 1.81],
+ '171.x': [202.17, 199.7],
+ '171.y': [31.18, 26.69],
+ '172.height': [4.2, 3.6],
+ '172.width': [3.74, 3.2],
+ '172.x': [200.32, 198.12],
+ '172.y': [27.06, 23.16],
+ '173.height': [1.79, 1.53],
+ '173.width': [1.53, 1.31],
+ '173.x': [200.38, 198.16],
+ '173.y': [29.88, 25.58],
+ '174.height': [1.55, 1.33],
+ '174.width': [1.41, 1.2],
+ '174.x': [201.72, 199.31],
+ '174.y': [30.15, 25.81],
+ '175.height': [1.14, 0.97],
+ '175.width': [1.07, 0.91],
+ '175.x': [126.23, 134.69],
+ '175.y': [68.99, 59.06],
+ '176.height': [11.19, 9.58],
+ '176.width': [10.95, 9.38],
+ '176.x': [207.71, 204.44],
+ '176.y': [67.26, 57.58],
+ '18.height': [31.79, 27.22],
+ '18.width': [249.21, 213.33],
+ '18.x': [61.86, 79.59],
+ '18.y': [0.67, 0.57],
+ '19.height': [4.06, 3.48],
+ '19.width': [1.83, 1.57],
+ '19.x': [113.33, 123.65],
+ '19.y': [48.84, 41.81],
+ '2.height': [8.16, 6.98],
+ '2.width': [7.96, 6.81],
+ '2.x': [169.21, 171.49],
+ '2.y': [48.1, 41.17],
+ '20.height': [1.18, 1.01],
+ '20.width': [2.56, 2.19],
+ '20.x': [137.49, 144.33],
+ '20.y': [143.91, 123.19],
+ '21.height': [14.61, 12.51],
+ '21.width': [16.74, 14.33],
+ '21.x': [188.52, 188.02],
+ '21.y': [0.86, 0.74],
+ '22.height': [13.77, 11.79],
+ '22.width': [36.79, 31.49],
+ '22.x': [141.08, 147.4],
+ '23.height': [1.16, 1],
+ '23.width': [1.73, 1.48],
+ '23.x': [239.29, 231.48],
+ '23.y': [141.42, 121.06],
+ '24.height': [1.43, 1.23],
+ '24.width': [2.32, 1.99],
+ '24.x': [303.75, 286.65],
+ '24.y': [93.6, 80.13],
+ '25.height': [15.28, 13.08],
+ '25.width': [15.06, 12.89],
+ '25.x': [199.7, 197.59],
+ '25.y': [111.04, 95.06],
+ '26.height': [2.39, 2.04],
+ '26.width': [2.15, 1.84],
+ '26.x': [209.15, 205.67],
+ '26.y': [119.04, 101.9],
+ '27.height': [22.29, 19.08],
+ '27.width': [23.64, 20.24],
+ '27.x': [81.12, 96.08],
+ '27.y': [42.07, 36.01],
+ '28.height': [5.69, 4.87],
+ '28.width': [4.65, 3.98],
+ '28.x': [133.5, 140.92],
+ '28.y': [120.79, 103.4],
+ '29.height': [48.88, 41.84],
+ '29.width': [37.36, 31.98],
+ '29.x': [114.58, 124.72],
+ '29.y': [76.22, 65.25],
+ '3.height': [31.79, 27.21],
+ '3.width': [66.09, 56.58],
+ '3.x': [83.08, 97.75],
+ '3.y': [0.1, 0.09],
+ '30.height': [16.5, 14.12],
+ '30.width': [12.23, 10.47],
+ '30.x': [119.13, 128.61],
+ '30.y': [95.51, 81.76],
+ '31.height': [23.34, 19.98],
+ '31.width': [13.07, 11.19],
+ '31.x': [107.32, 118.51],
+ '31.y': [83.04, 71.09],
+ '32.height': [21.48, 18.39],
+ '32.width': [11.57, 9.9],
+ '32.x': [109.52, 120.39],
+ '32.y': [67.02, 57.37],
+ '33.height': [3.06, 2.62],
+ '33.width': [5.42, 4.64],
+ '33.x': [106.1, 117.46],
+ '33.y': [70.63, 60.46],
+ '34.height': [3.82, 3.27],
+ '34.width': [3.06, 2.62],
+ '34.x': [103.57, 115.3],
+ '34.y': [68.58, 58.7],
+ '35.height': [5.45, 4.67],
+ '35.width': [4.6, 3.94],
+ '35.x': [102.23, 114.14],
+ '35.y': [63.75, 54.57],
+ '36.height': [3.82, 3.27],
+ '36.width': [5.94, 5.08],
+ '36.x': [100.89, 113],
+ '36.y': [62.5, 53.5],
+ '37.height': [1.62, 1.39],
+ '37.width': [2.24, 1.92],
+ '37.x': [100.07, 112.3],
+ '37.y': [64.5, 55.21],
+ '38.height': [5.15, 4.41],
+ '38.width': [3.99, 3.41],
+ '38.x': [98.21, 110.7],
+ '38.y': [60.22, 51.55],
+ '39.height': [3.28, 2.81],
+ '39.width': [1.57, 1.34],
+ '39.x': [101.28, 113.34],
+ '39.y': [59.37, 50.82],
+ '4.height': [53.48, 45.78],
+ '4.width': [92.66, 79.32],
+ '4.x': [37.65, 58.86],
+ '4.y': [5.37, 4.59],
+ '40.height': [14.66, 12.55],
+ '40.width': [12.81, 10.97],
+ '40.x': [115.37, 125.39],
+ '40.y': [67.37, 57.67],
+ '41.height': [9.12, 7.81],
+ '41.width': [4.52, 3.87],
+ '41.x': [126.46, 134.89],
+ '41.y': [72.22, 61.82],
+ '42.height': [5.41, 4.63],
+ '42.width': [3.96, 3.39],
+ '42.x': [129.59, 137.57],
+ '42.y': [75.22, 64.39],
+ '43.height': [58.34, 49.94],
+ '43.width': [60.08, 51.43],
+ '43.x': [132.91, 140.41],
+ '43.y': [21.98, 18.82],
+ '44.height': [8.16, 6.98],
+ '44.width': [5.45, 4.67],
+ '44.x': [107.65, 118.78],
+ '44.y': [81.19, 69.5],
+ '45.height': [0.72, 0.62],
+ '45.width': [1.53, 1.31],
+ '45.x': [122.29, 131.32],
+ '45.y': [59.34, 50.8],
+ '46.height': [1.03, 0.88],
+ '46.width': [1.93, 1.65],
+ '46.x': [111.93, 122.45],
+ '46.y': [59.34, 50.8],
+ '47.height': [4.13, 3.54],
+ '47.width': [9.61, 8.23],
+ '47.x': [106.6, 117.89],
+ '47.y': [53.54, 45.84],
+ '48.height': [8.46, 7.24],
+ '48.width': [7.16, 6.13],
+ '48.x': [208.59, 205.2],
+ '48.y': [102.81, 88.01],
+ '49.height': [11.32, 9.69],
+ '49.width': [8.96, 7.67],
+ '49.x': [203.17, 200.56],
+ '49.y': [105.52, 90.33],
+ '5.height': [15.08, 12.91],
+ '5.width': [32.26, 27.61],
+ '5.x': [222.05, 216.71],
+ '5.y': [17.92, 15.34],
+ '50.height': [14.9, 12.75],
+ '50.width': [12.46, 10.67],
+ '50.x': [195.97, 194.39],
+ '50.y': [104.62, 89.56],
+ '51.height': [5.4, 4.62],
+ '51.width': [5.75, 4.92],
+ '51.x': [168.42, 170.81],
+ '51.y': [61.76, 52.87],
+ '52.height': [18.65, 15.97],
+ '52.width': [15.45, 13.22],
+ '52.x': [173.55, 175.2],
+ '52.y': [51.35, 43.96],
+ '53.height': [15.85, 13.57],
+ '53.width': [11.29, 9.67],
+ '53.x': [169.21, 171.49],
+ '53.y': [48.41, 41.44],
+ '54.height': [7.79, 6.67],
+ '54.width': [2.87, 2.45],
+ '54.x': [185.73, 185.63],
+ '54.y': [67.28, 57.59],
+ '55.height': [14.81, 12.68],
+ '55.width': [14.46, 12.38],
+ '55.x': [185.28, 185.24],
+ '55.y': [53.2, 45.54],
+ '56.height': [12.3, 10.53],
+ '56.width': [11.22, 9.61],
+ '56.x': [187.56, 187.2],
+ '56.y': [65.21, 55.82],
+ '57.height': [14.26, 12.21],
+ '57.width': [7.2, 6.16],
+ '57.x': [193.1, 191.93],
+ '57.y': [66.49, 56.91],
+ '58.height': [6.51, 5.58],
+ '58.width': [1.83, 1.56],
+ '58.y': [68.83, 58.92],
+ '59.height': [8.18, 7],
+ '59.width': [4.1, 3.51],
+ '59.x': [181.91, 182.35],
+ '59.y': [68.73, 58.83],
+ '6.height': [9.28, 7.94],
+ '6.width': [15.73, 13.47],
+ '6.x': [230.81, 224.22],
+ '6.y': [27.68, 23.69],
+ '60.height': [7.93, 6.79],
+ '60.width': [5.76, 4.93],
+ '60.x': [176.8, 177.98],
+ '60.y': [69.46, 59.46],
+ '61.height': [6.74, 5.77],
+ '61.width': [6.89, 5.89],
+ '61.x': [170.67, 172.73],
+ '61.y': [66.83, 57.21],
+ '62.height': [2.02, 1.73],
+ '62.width': [2.82, 2.41],
+ '62.x': [169.24, 171.51],
+ '62.y': [66.78, 57.17],
+ '63.height': [5.37, 4.6],
+ '63.width': [3.7, 3.17],
+ '63.x': [174.1, 175.67],
+ '63.y': [71.99, 61.63],
+ '64.height': [4.18, 3.57],
+ '64.width': [2.86, 2.45],
+ '64.x': [172.41, 174.23],
+ '64.y': [70.07, 59.98],
+ '65.height': [7, 5.99],
+ '65.width': [7.24, 6.2],
+ '65.x': [179.82, 180.56],
+ '65.y': [63.63, 54.47],
+ '66.height': [11.38, 9.74],
+ '66.width': [12.32, 10.55],
+ '66.x': [198.79, 196.8],
+ '66.y': [68.67, 58.78],
+ '67.height': [11.28, 9.66],
+ '67.width': [7.03, 6.02],
+ '67.x': [195.59, 194.07],
+ '67.y': [78.17, 66.92],
+ '68.height': [8.12, 6.95],
+ '68.width': [5.38, 4.61],
+ '68.x': [193.41, 192.2],
+ '68.y': [79.98, 68.46],
+ '69.height': [1.64, 1.4],
+ '69.width': [1.89, 1.62],
+ '69.x': [193.89, 192.61],
+ '69.y': [80.03, 68.51],
+ '7.height': [10.46, 8.95],
+ '7.width': [14.67, 12.56],
+ '7.x': [318.97, 299.68],
+ '7.y': [86.19, 73.78],
+ '70.height': [12.34, 10.56],
+ '70.width': [11.22, 9.6],
+ '70.x': [205.53, 202.57],
+ '70.y': [93.56, 80.09],
+ '71.height': [9.61, 8.23],
+ '71.width': [2.76, 2.37],
+ '71.x': [215.81, 211.38],
+ '71.y': [94.83, 81.18],
+ '72.height': [20.52, 17.57],
+ '72.width': [9.97, 8.54],
+ '72.x': [213.39, 209.3],
+ '72.y': [96.22, 82.36],
+ '73.height': [1.97, 1.69],
+ '73.width': [1.26, 1.08],
+ '73.x': [212.82, 208.81],
+ '73.y': [115.42, 98.81],
+ '74.height': [17.18, 14.7],
+ '74.width': [11.86, 10.16],
+ '74.x': [195.9, 194.33],
+ '74.y': [88.68, 75.91],
+ '75.height': [2.77, 2.37],
+ '75.width': [1.64, 1.41],
+ '75.x': [212.72, 208.73],
+ '75.y': [85.99, 73.61],
+ '76.height': [4.47, 3.82],
+ '76.width': [1.14, 0.98],
+ '76.x': [215.46, 211.07],
+ '76.y': [41.41, 35.45],
+ '77.height': [1.81, 1.55],
+ '77.width': [1.12, 0.96],
+ '77.x': [215.94, 211.49],
+ '77.y': [39.82, 34.09],
+ '78.height': [16.94, 14.5],
+ '78.width': [7.56, 6.47],
+ '78.x': [224.87, 219.13],
+ '78.y': [98.41, 84.24],
+ '79.height': [1.39, 1.19],
+ '79.width': [0.52, 0.44],
+ '79.x': [215.99, 211.53],
+ '79.y': [42.29, 36.2],
+ '8.height': [20.35, 17.42],
+ '8.width': [43.81, 37.51],
+ '8.x': [275.88, 262.79],
+ '8.y': [75.92, 64.99],
+ '80.height': [0.95, 0.81],
+ '80.width': [2.84, 2.43],
+ '80.x': [169.11, 171.39],
+ '80.y': [65.2, 55.81],
+ '81.height': [8.2, 7.02],
+ '81.width': [3.52, 3.02],
+ '81.x': [191.59, 190.65],
+ '81.y': [36.72, 31.43],
+ '82.height': [21.69, 18.57],
+ '82.width': [18.87, 16.15],
+ '82.x': [177.15, 178.28],
+ '82.y': [36.98, 31.66],
+ '83.height': [4.95, 4.24],
+ '83.width': [3.39, 2.91],
+ '83.x': [216.29, 211.78],
+ '83.y': [41.3, 35.35],
+ '84.height': [4.36, 3.73],
+ '84.width': [4.28, 3.66],
+ '84.x': [232.2, 225.41],
+ '84.y': [50.04, 42.83],
+ '85.height': [1.9, 1.63],
+ '85.width': [0.83, 0.71],
+ '85.x': [231.23, 224.57],
+ '85.y': [49.96, 42.77],
+ '86.height': [1.84, 1.57],
+ '86.width': [1.76, 1.51],
+ '86.x': [226.8, 220.78],
+ '86.y': [45.21, 38.71],
+ '87.height': [9.68, 8.29],
+ '87.width': [9.31, 7.97],
+ '87.x': [219.12, 214.21],
+ '87.y': [36.68, 31.4],
+ '88.height': [12.07, 10.33],
+ '88.width': [6.7, 5.74],
+ '88.x': [233.4, 226.43],
+ '88.y': [49.62, 42.48],
+ '89.height': [2.49, 2.13],
+ '89.width': [0.73, 0.62],
+ '89.x': [341.53, 319],
+ '89.y': [101.7, 87.05],
+ '9.height': [37.17, 31.82],
+ '9.width': [14.17, 12.13],
+ '9.x': [122.23, 131.27],
+ '9.y': [110.72, 94.78],
+ '90.height': [5.19, 4.44],
+ '90.width': [5.05, 4.33],
+ '90.x': [281.54, 267.64],
+ '90.y': [64.32, 55.06],
+ '91.height': [18.67, 15.99],
+ '91.width': [8.51, 7.29],
+ '91.x': [275.77, 262.7],
+ '91.y': [56.97, 48.77],
+ '92.height': [10.75, 9.2],
+ '92.width': [8.33, 7.13],
+ '92.x': [277.8, 264.44],
+ '92.y': [54.44, 46.6],
+ '93.height': [22.94, 19.64],
+ '93.width': [8.92, 7.64],
+ '93.x': [270.27, 258],
+ '93.y': [47.28, 40.47],
+ '94.height': [18.58, 15.9],
+ '94.width': [9.25, 7.92],
+ '94.x': [279.09, 265.54],
+ '94.y': [53.34, 45.66],
+ '95.height': [5.9, 5.05],
+ '95.width': [4.38, 3.75],
+ '95.x': [290.31, 275.15],
+ '95.y': [30.46, 26.07],
+ '96.height': [4.83, 4.13],
+ '96.width': [4.19, 3.58],
+ '96.x': [293.34, 277.74],
+ '96.y': [35.29, 30.21],
+ '97.height': [10.87, 9.31],
+ '97.width': [27.23, 23.31],
+ '97.x': [254.57, 244.56],
+ '97.y': [21.1, 18.06],
+ '98.height': [33.89, 29.01],
+ '98.width': [25.41, 21.75],
+ '98.x': [247.52, 238.52],
+ '98.y': [38.84, 33.25],
+ '99.height': [7.1, 6.08],
+ '99.width': [5.37, 4.6],
+ '99.x': [265.27, 253.71],
+ '99.y': [49.56, 42.43],
+ },
+ },
+ style: {},
+ },
+ '../gallery/maps.stories.tsx.worldEqualEarthSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1105, 1174.58],
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/maps.stories.tsx.worldMercatorSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'mapSize.height': [166.7, 142.7],
+ },
+ area: {
+ 'rect.height': [166.7, 142.7],
+ },
+ chrome: {
+ bottomAnchorY: [324, 338],
+ bottomHeight: [66, 52],
+ 'byline.maxWidth': [235, 304.58],
+ 'byline.y': [42, 28],
+ 'source.maxWidth': [235, 304.58],
+ },
+ marks: {
+ '0.height': [1.7, 1.45],
+ '0.width': [257.72, 220.62],
+ '0.x': [56.14, 74.69],
+ '0.y': [130.21, 111.46],
+ '1.height': [7.77, 6.65],
+ '1.width': [7.86, 6.73],
+ '1.x': [206, 202.98],
+ '1.y': [119.27, 102.1],
+ '10.height': [35.33, 30.24],
+ '10.width': [6.22, 5.32],
+ '10.x': [130.85, 138.64],
+ '10.y': [131.37, 112.46],
+ '100.height': [1.27, 1.09],
+ '100.width': [2.36, 2.02],
+ '100.x': [248.58, 239.43],
+ '100.y': [97.45, 83.42],
+ '101.height': [3.28, 2.81],
+ '101.width': [5.79, 4.96],
+ '101.x': [242.33, 234.08],
+ '101.y': [95.71, 81.93],
+ '102.height': [11.2, 9.59],
+ '102.width': [12.14, 10.4],
+ '102.x': [228.58, 222.31],
+ '102.y': [89.92, 76.97],
+ '103.height': [7.92, 6.78],
+ '103.width': [10.47, 8.97],
+ '103.x': [228.33, 222.09],
+ '103.y': [88.69, 75.92],
+ '104.height': [3.88, 3.32],
+ '104.width': [5.4, 4.62],
+ '104.x': [233.28, 226.33],
+ '104.y': [86.39, 73.95],
+ '105.height': [3.83, 3.28],
+ '105.width': [7.73, 6.62],
+ '105.x': [234.73, 227.57],
+ '105.y': [84.13, 72.02],
+ '106.height': [6.9, 5.91],
+ '106.width': [10.05, 8.61],
+ '106.x': [222.59, 217.17],
+ '106.y': [84.67, 72.48],
+ '107.height': [12.47, 10.67],
+ '107.width': [13.75, 11.77],
+ '107.x': [216.58, 212.03],
+ '107.y': [87.56, 74.96],
+ '108.height': [4.29, 3.67],
+ '108.width': [4.76, 4.07],
+ '108.x': [210.56, 206.88],
+ '108.y': [89.83, 76.9],
+ '109.height': [2.34, 2.01],
+ '109.width': [2.09, 1.79],
+ '109.x': [216.2, 211.71],
+ '109.y': [86.12, 73.72],
+ '11.height': [13.34, 11.42],
+ '11.width': [13.6, 11.64],
+ '11.x': [193.72, 192.46],
+ '11.y': [114.82, 98.29],
+ '110.height': [21.55, 18.45],
+ '110.width': [9.22, 7.89],
+ '110.x': [192.9, 191.76],
+ '110.y': [49.24, 42.15],
+ '111.height': [5.88, 5.03],
+ '111.width': [6.8, 5.82],
+ '111.x': [201.61, 199.22],
+ '111.y': [69.76, 59.72],
+ '112.height': [7.67, 6.57],
+ '112.width': [12.88, 11.03],
+ '112.x': [200.81, 198.53],
+ '112.y': [74.47, 63.74],
+ '113.height': [6.78, 5.8],
+ '113.width': [7.13, 6.1],
+ '113.x': [195.08, 193.63],
+ '113.y': [71.43, 61.14],
+ '114.height': [2.78, 2.38],
+ '114.width': [5.37, 4.6],
+ '114.x': [191.79, 190.81],
+ '114.y': [78.19, 66.93],
+ '115.height': [3.02, 2.59],
+ '115.width': [4.66, 3.99],
+ '115.x': [196.6, 194.93],
+ '115.y': [78.64, 67.32],
+ '116.height': [3.13, 2.68],
+ '116.width': [2.44, 2.09],
+ '116.x': [204.06, 201.31],
+ '116.y': [78.81, 67.47],
+ '117.height': [4.67, 4],
+ '117.width': [6.73, 5.76],
+ '117.x': [199.48, 197.39],
+ '117.y': [79.08, 67.69],
+ '118.height': [3.09, 2.65],
+ '118.width': [3.96, 3.39],
+ '118.x': [200.07, 197.9],
+ '118.y': [69.5, 59.49],
+ '119.height': [3.08, 2.64],
+ '119.width': [5.1, 4.36],
+ '119.x': [200.07, 197.9],
+ '119.y': [67.39, 57.69],
+ '12.height': [9.88, 8.46],
+ '12.width': [7.27, 6.22],
+ '12.x': [214.34, 210.11],
+ '12.y': [109.91, 94.09],
+ '120.height': [2.93, 2.51],
+ '120.width': [3.43, 2.94],
+ '120.x': [201.71, 199.3],
+ '120.y': [65.12, 55.75],
+ '121.height': [8.79, 7.53],
+ '121.width': [6.46, 5.53],
+ '121.x': [189.29, 188.67],
+ '121.y': [71.26, 61],
+ '122.height': [2.93, 2.5],
+ '122.width': [4.42, 3.79],
+ '122.x': [201.02, 198.71],
+ '122.y': [83.2, 71.22],
+ '123.height': [6.32, 5.41],
+ '123.width': [4.62, 3.95],
+ '123.x': [199.43, 197.35],
+ '123.y': [85.56, 73.24],
+ '124.height': [5.83, 4.99],
+ '124.width': [13.42, 11.49],
+ '124.x': [203.64, 200.96],
+ '124.y': [85.26, 72.98],
+ '125.height': [2.91, 2.49],
+ '125.width': [1.23, 1.05],
+ '125.x': [198.82, 196.83],
+ '125.y': [84.73, 72.53],
+ '126.height': [4.04, 3.46],
+ '126.width': [4.11, 3.51],
+ '126.x': [194.78, 193.37],
+ '126.y': [80.89, 69.25],
+ '127.height': [2.15, 1.84],
+ '127.width': [3.16, 2.71],
+ '127.x': [189.31, 188.69],
+ '127.y': [79.49, 68.05],
+ '128.height': [0.76, 0.65],
+ '128.width': [0.41, 0.35],
+ '128.x': [189.06, 188.48],
+ '128.y': [76.99, 65.9],
+ '129.height': [2.19, 1.87],
+ '129.width': [2.61, 2.23],
+ '129.x': [186.8, 186.54],
+ '129.y': [75.46, 64.6],
+ '13.height': [7.3, 6.25],
+ '13.width': [5.7, 4.88],
+ '13.x': [209.26, 205.77],
+ '13.y': [114.64, 98.13],
+ '130.height': [3.16, 2.7],
+ '130.width': [2.7, 2.31],
+ '130.x': [187.37, 187.03],
+ '130.y': [73.07, 62.55],
+ '131.height': [5.06, 4.33],
+ '131.width': [2.25, 1.92],
+ '131.x': [178.18, 179.16],
+ '131.y': [85.13, 72.87],
+ '132.height': [7.29, 6.24],
+ '132.width': [8.9, 7.62],
+ '132.x': [178.27, 179.24],
+ '132.y': [83.69, 71.64],
+ '133.height': [4.16, 3.56],
+ '133.width': [2.82, 2.42],
+ '133.x': [177.86, 178.89],
+ '133.y': [71.08, 60.84],
+ '134.height': [1.76, 1.51],
+ '134.width': [2.21, 1.89],
+ '134.x': [302.43, 285.52],
+ '134.y': [133.28, 114.1],
+ '135.height': [3.06, 2.62],
+ '135.width': [4.23, 3.62],
+ '135.x': [297.03, 280.9],
+ '135.y': [123.32, 105.57],
+ '136.height': [11.54, 9.88],
+ '136.width': [8.6, 7.36],
+ '136.x': [304.2, 287.04],
+ '136.y': [144.89, 124.03],
+ '137.height': [27.1, 23.2],
+ '137.width': [28.8, 24.65],
+ '137.x': [266.14, 254.46],
+ '137.y': [126.27, 108.09],
+ '138.height': [2.79, 2.39],
+ '138.width': [1.5, 1.28],
+ '138.x': [242.05, 233.84],
+ '138.y': [111.52, 95.46],
+ '139.height': [32.2, 27.57],
+ '139.width': [43.92, 37.6],
+ '139.x': [237.74, 230.15],
+ '139.y': [73.13, 62.6],
+ '14.height': [10.24, 8.77],
+ '14.width': [11.79, 10.1],
+ '14.x': [200.7, 198.44],
+ '14.y': [102.43, 87.69],
+ '140.height': [2.6, 2.23],
+ '140.width': [1.32, 1.13],
+ '140.x': [270.98, 258.6],
+ '140.y': [99.86, 85.48],
+ '141.height': [10.13, 8.67],
+ '141.width': [8.4, 7.19],
+ '141.x': [189.83, 189.14],
+ '141.y': [80.25, 68.7],
+ '142.height': [3.78, 3.23],
+ '142.width': [3.29, 2.82],
+ '142.x': [190.79, 189.96],
+ '142.y': [67.71, 57.96],
+ '143.height': [10.69, 9.15],
+ '143.width': [6.63, 5.67],
+ '143.x': [179.58, 180.36],
+ '143.y': [66.48, 56.91],
+ '144.height': [5.14, 4.4],
+ '144.width': [7.67, 6.57],
+ '144.x': [167.58, 170.09],
+ '144.y': [54.13, 46.34],
+ '145.height': [3.36, 2.88],
+ '145.width': [4.01, 3.43],
+ '145.x': [217.07, 212.45],
+ '145.y': [85.53, 73.22],
+ '146.height': [2.41, 2.06],
+ '146.width': [4.78, 4.09],
+ '146.x': [213.6, 209.49],
+ '146.y': [83.88, 71.8],
+ '147.height': [9.48, 8.12],
+ '147.width': [6.7, 5.74],
+ '147.x': [268.88, 256.81],
+ '147.y': [105.1, 89.97],
+ '148.height': [4.42, 3.78],
+ '148.width': [13.67, 11.7],
+ '148.x': [256.65, 246.34],
+ '148.y': [113.61, 97.26],
+ '149.height': [1.03, 0.89],
+ '149.width': [0.89, 0.76],
+ '149.x': [266.76, 254.99],
+ '149.y': [114.68, 98.17],
+ '15.height': [11.92, 10.2],
+ '15.width': [7.41, 6.34],
+ '15.x': [194.69, 193.3],
+ '15.y': [101.34, 86.75],
+ '150.height': [1.45, 1.24],
+ '150.width': [2.05, 1.76],
+ '150.x': [194.81, 193.39],
+ '150.y': [80.53, 68.93],
+ '151.height': [17.73, 15.18],
+ '151.width': [7.78, 6.66],
+ '151.x': [199.78, 197.65],
+ '151.y': [47.06, 40.28],
+ '152.height': [1.97, 1.68],
+ '152.width': [4.07, 3.48],
+ '152.x': [197.08, 195.34],
+ '152.y': [77.61, 66.43],
+ '153.height': [2.85, 2.44],
+ '153.width': [4.73, 4.05],
+ '153.x': [193.76, 192.5],
+ '153.y': [75.87, 64.95],
+ '154.height': [4.11, 3.52],
+ '154.width': [4.84, 4.14],
+ '154.x': [211, 207.26],
+ '154.y': [105.49, 90.3],
+ '155.height': [13.32, 11.41],
+ '155.width': [11.55, 9.89],
+ '155.x': [277.64, 264.3],
+ '155.y': [81.87, 70.09],
+ '156.height': [6.41, 5.49],
+ '156.width': [6.01, 5.14],
+ '156.x': [140.12, 146.58],
+ '156.y': [132.7, 113.6],
+ '157.height': [4.77, 4.09],
+ '157.width': [7.52, 6.44],
+ '157.x': [215.5, 211.11],
+ '157.y': [104.73, 89.65],
+ '158.height': [12.47, 10.68],
+ '158.width': [15.06, 12.89],
+ '158.x': [209.79, 206.22],
+ '158.y': [94.25, 80.68],
+ '159.height': [69.91, 59.84],
+ '159.width': [257.72, 220.62],
+ '159.x': [56.14, 74.69],
+ '159.y': [177.54, 151.98],
+ '16.height': [1.43, 1.22],
+ '16.width': [2.03, 1.74],
+ '16.x': [131.7, 139.37],
+ '16.y': [104.03, 89.06],
+ '160.height': [0.59, 0.5],
+ '160.width': [1.32, 1.13],
+ '160.x': [208.43, 205.06],
+ '160.y': [91.22, 78.09],
+ '161.height': [0.53, 0.45],
+ '161.width': [1.25, 1.07],
+ '161.x': [208.09, 204.77],
+ '161.y': [91.66, 78.46],
+ '162.height': [11.74, 10.05],
+ '162.width': [11.38, 9.74],
+ '162.x': [172.82, 174.57],
+ '162.y': [91.14, 78.02],
+ '163.height': [7.7, 6.59],
+ '163.width': [8.71, 7.45],
+ '163.x': [202.68, 200.14],
+ '163.y': [94.73, 81.1],
+ '164.height': [10.87, 9.3],
+ '164.width': [11.34, 9.71],
+ '164.x': [191.67, 190.71],
+ '164.y': [93.42, 79.97],
+ '165.height': [8.38, 7.18],
+ '165.width': [10.62, 9.09],
+ '165.x': [208.59, 205.19],
+ '165.y': [107.75, 92.24],
+ '166.height': [1.3, 1.11],
+ '166.width': [1.19, 1.01],
+ '166.x': [214.82, 210.53],
+ '166.y': [109.42, 93.67],
+ '167.height': [2.52, 2.16],
+ '167.width': [4.57, 3.92],
+ '167.x': [215.47, 211.08],
+ '167.y': [110.33, 94.44],
+ '168.height': [4.08, 3.49],
+ '168.width': [3.91, 3.34],
+ '168.x': [206.18, 203.13],
+ '168.y': [115.54, 98.91],
+ '169.height': [1.28, 1.09],
+ '169.width': [1.28, 1.1],
+ '169.x': [205.78, 202.79],
+ '169.y': [119.4, 102.21],
+ '17.height': [1.73, 1.48],
+ '17.width': [2.6, 2.22],
+ '17.x': [133.5, 140.91],
+ '17.y': [104.06, 89.08],
+ '170.height': [2.57, 2.2],
+ '170.width': [2.76, 2.36],
+ '170.x': [196.27, 194.65],
+ '170.y': [82.2, 70.36],
+ '171.height': [1.41, 1.21],
+ '171.width': [1.78, 1.52],
+ '171.x': [199.65, 197.54],
+ '171.y': [85.09, 72.84],
+ '172.height': [3.92, 3.36],
+ '172.width': [2.97, 2.55],
+ '172.x': [198.48, 196.54],
+ '172.y': [81.24, 69.54],
+ '173.height': [1.6, 1.37],
+ '173.width': [1.35, 1.16],
+ '173.x': [198.21, 196.31],
+ '173.y': [83.91, 71.83],
+ '174.height': [1.39, 1.19],
+ '174.width': [1.22, 1.04],
+ '174.x': [199.37, 197.3],
+ '174.y': [84.16, 72.04],
+ '175.height': [0.65, 0.55],
+ '175.width': [0.76, 0.65],
+ '175.x': [140.65, 147.03],
+ '175.y': [110.74, 94.8],
+ '176.height': [6.32, 5.41],
+ '176.width': [8.17, 6.99],
+ '176.x': [202.1, 199.64],
+ '176.y': [109.75, 93.95],
+ '18.height': [73.05, 62.54],
+ '18.width': [257.72, 220.62],
+ '18.x': [56.14, 74.69],
+ '18.y': [13.16, 11.26],
+ '19.height': [2.64, 2.26],
+ '19.width': [1.42, 1.21],
+ '19.x': [128.46, 136.6],
+ '19.y': [98.47, 84.29],
+ '2.height': [5.23, 4.48],
+ '2.width': [6.01, 5.15],
+ '2.x': [172.79, 174.54],
+ '2.y': [97.97, 83.87],
+ '20.height': [1.39, 1.19],
+ '20.width': [2.47, 2.11],
+ '20.x': [141.19, 147.5],
+ '20.y': [161.28, 138.06],
+ '21.height': [51.39, 43.99],
+ '21.width': [18.83, 16.12],
+ '21.x': [188.57, 188.06],
+ '21.y': [15.85, 13.57],
+ '22.height': [64.51, 55.23],
+ '22.width': [43.73, 37.44],
+ '22.x': [132.53, 140.08],
+ '23.height': [1.26, 1.08],
+ '23.width': [1.32, 1.13],
+ '23.x': [234.2, 227.11],
+ '23.y': [158.53, 135.71],
+ '24.height': [0.81, 0.69],
+ '24.width': [1.69, 1.45],
+ '24.x': [274.46, 261.58],
+ '24.y': [124.53, 106.6],
+ '25.height': [10.4, 8.9],
+ '25.width': [11.8, 10.1],
+ '25.x': [196.7, 195.02],
+ '25.y': [134.81, 115.4],
+ '26.height': [1.65, 1.41],
+ '26.width': [1.66, 1.43],
+ '26.x': [204.33, 201.55],
+ '26.y': [140.01, 119.85],
+ '27.height': [14.29, 12.23],
+ '27.width': [21.7, 18.58],
+ '27.x': [101.15, 113.22],
+ '27.y': [93.77, 80.27],
+ '28.height': [4.11, 3.52],
+ '28.width': [3.73, 3.2],
+ '28.x': [143.17, 149.19],
+ '28.y': [141.21, 120.88],
+ '29.height': [29.47, 25.23],
+ '29.width': [28.1, 24.06],
+ '29.x': [132.03, 139.66],
+ '29.y': [114.83, 98.3],
+ '3.height': [83.13, 71.16],
+ '3.width': [63.25, 54.14],
+ '3.x': [84.06, 98.59],
+ '3.y': [2.58, 2.21],
+ '30.height': [9.8, 8.39],
+ '30.width': [8.66, 7.41],
+ '30.x': [135.18, 142.35],
+ '30.y': [125.61, 107.53],
+ '31.height': [13.33, 11.41],
+ '31.width': [9.12, 7.81],
+ '31.x': [126.72, 135.11],
+ '31.y': [118.63, 101.55],
+ '32.height': [12.05, 10.32],
+ '32.width': [8.67, 7.42],
+ '32.x': [128.45, 136.59],
+ '32.y': [109.61, 93.83],
+ '33.height': [1.73, 1.48],
+ '33.width': [4.1, 3.51],
+ '33.x': [125.61, 134.16],
+ '33.y': [111.67, 95.6],
+ '34.height': [2.17, 1.86],
+ '34.width': [2.43, 2.08],
+ '34.x': [123.48, 132.33],
+ '34.y': [110.5, 94.6],
+ '35.height': [3.15, 2.7],
+ '35.width': [3.24, 2.77],
+ '35.x': [122.24, 131.27],
+ '35.y': [107.71, 92.2],
+ '36.height': [2.23, 1.91],
+ '36.width': [4.44, 3.8],
+ '36.x': [121.03, 130.24],
+ '36.y': [106.98, 91.58],
+ '37.height': [0.94, 0.81],
+ '37.width': [1.7, 1.45],
+ '37.x': [120.5, 129.79],
+ '37.y': [108.15, 92.58],
+ '38.height': [3.04, 2.6],
+ '38.width': [2.87, 2.45],
+ '38.x': [118.98, 128.48],
+ '38.y': [105.62, 90.41],
+ '39.height': [1.96, 1.68],
+ '39.width': [0.8, 0.69],
+ '39.x': [121.12, 130.32],
+ '39.y': [105.11, 89.97],
+ '4.height': [60.32, 51.64],
+ '4.width': [75.04, 64.24],
+ '4.x': [62.02, 79.72],
+ '4.y': [44.47, 38.06],
+ '40.height': [8.25, 7.07],
+ '40.width': [9.7, 8.3],
+ '40.x': [132.52, 140.08],
+ '40.y': [109.81, 94],
+ '41.height': [5.1, 4.37],
+ '41.width': [3.49, 2.98],
+ '41.x': [141.04, 147.37],
+ '41.y': [112.58, 96.37],
+ '42.height': [3.02, 2.59],
+ '42.width': [2.93, 2.5],
+ '42.x': [143.45, 149.43],
+ '42.y': [114.26, 97.81],
+ '43.height': [41.28, 35.34],
+ '43.width': [45.88, 39.27],
+ '43.x': [145.97, 151.59],
+ '43.y': [75.84, 64.92],
+ '44.height': [4.54, 3.89],
+ '44.width': [4.1, 3.51],
+ '44.x': [127.04, 135.38],
+ '44.y': [117.6, 100.67],
+ '45.height': [0.43, 0.37],
+ '45.width': [1.18, 1.01],
+ '45.x': [136.86, 143.79],
+ '45.y': [105.09, 89.96],
+ '46.height': [0.62, 0.53],
+ '46.width': [1.53, 1.31],
+ '46.x': [128.92, 136.99],
+ '46.y': [105.09, 89.96],
+ '47.height': [2.57, 2.2],
+ '47.width': [7.73, 6.62],
+ '47.x': [124.17, 132.93],
+ '47.y': [101.51, 86.9],
+ '48.height': [5.12, 4.38],
+ '48.width': [5.43, 4.65],
+ '48.x': [203.09, 200.48],
+ '48.y': [129.83, 111.14],
+ '49.height': [7.1, 6.08],
+ '49.width': [6.83, 5.84],
+ '49.x': [199.24, 197.19],
+ '49.y': [131.44, 112.51],
+ '5.height': [15.91, 13.62],
+ '5.width': [29.27, 25.06],
+ '5.x': [218.27, 213.48],
+ '5.y': [70.76, 60.57],
+ '50.height': [9.44, 8.08],
+ '50.width': [9.56, 8.18],
+ '50.x': [193.4, 192.19],
+ '50.y': [130.9, 112.05],
+ '51.height': [3.15, 2.7],
+ '51.width': [4.41, 3.77],
+ '51.x': [172.38, 174.2],
+ '51.y': [106.53, 91.2],
+ '52.height': [11.21, 9.59],
+ '52.width': [11.77, 10.08],
+ '52.x': [176.29, 177.54],
+ '52.y': [100.11, 85.7],
+ '53.height': [9.83, 8.41],
+ '53.width': [8.69, 7.44],
+ '53.x': [172.79, 174.54],
+ '53.y': [98.18, 84.05],
+ '54.height': [4.42, 3.78],
+ '54.width': [2.16, 1.85],
+ '54.x': [185.55, 185.47],
+ '54.y': [109.76, 93.96],
+ '55.height': [8.89, 7.61],
+ '55.width': [11.17, 9.56],
+ '55.x': [185.21, 185.18],
+ '55.y': [101.29, 86.71],
+ '56.height': [6.99, 5.98],
+ '56.width': [8.51, 7.28],
+ '56.x': [186.93, 186.65],
+ '56.y': [108.56, 92.93],
+ '57.height': [8.05, 6.89],
+ '57.width': [5.39, 4.61],
+ '57.x': [191.08, 190.2],
+ '57.y': [109.3, 93.57],
+ '58.height': [3.69, 3.15],
+ '58.width': [1.37, 1.17],
+ '58.y': [110.65, 94.72],
+ '59.height': [4.62, 3.95],
+ '59.width': [3.08, 2.64],
+ '59.x': [182.68, 183.01],
+ '59.y': [110.59, 94.67],
+ '6.height': [8.07, 6.91],
+ '6.width': [12.26, 10.49],
+ '6.x': [225.04, 219.27],
+ '6.y': [81.84, 70.06],
+ '60.height': [4.47, 3.83],
+ '60.width': [4.32, 3.7],
+ '60.x': [178.84, 179.73],
+ '60.y': [111.01, 95.03],
+ '61.height': [3.84, 3.28],
+ '61.width': [5.22, 4.47],
+ '61.x': [174.17, 175.73],
+ '61.y': [109.5, 93.74],
+ '62.height': [1.16, 0.99],
+ '62.width': [2.13, 1.82],
+ '62.x': [173.06, 174.78],
+ '62.y': [109.47, 93.71],
+ '63.height': [3.02, 2.58],
+ '63.width': [2.79, 2.39],
+ '63.x': [176.81, 177.99],
+ '63.y': [112.45, 96.26],
+ '64.height': [2.36, 2.02],
+ '64.width': [2.16, 1.85],
+ '64.x': [175.52, 176.88],
+ '64.y': [111.36, 95.32],
+ '65.height': [4.04, 3.46],
+ '65.width': [5.47, 4.69],
+ '65.x': [181.08, 181.65],
+ '65.y': [107.64, 92.14],
+ '66.height': [6.4, 5.48],
+ '66.width': [9.24, 7.91],
+ '66.x': [195.35, 193.86],
+ '66.y': [110.56, 94.64],
+ '67.height': [6.28, 5.38],
+ '67.width': [5.27, 4.51],
+ '67.x': [192.94, 191.8],
+ '67.y': [115.92, 99.23],
+ '68.height': [4.52, 3.87],
+ '68.width': [4.03, 3.45],
+ '68.x': [191.3, 190.39],
+ '68.y': [116.92, 100.09],
+ '69.height': [0.91, 0.78],
+ '69.width': [1.42, 1.21],
+ '69.x': [191.66, 190.7],
+ '69.y': [116.95, 100.11],
+ '7.height': [5.88, 5.03],
+ '7.width': [10.75, 9.2],
+ '7.x': [285.94, 271.41],
+ '7.y': [120.38, 103.05],
+ '70.height': [7.16, 6.13],
+ '70.width': [8.3, 7.11],
+ '70.x': [200.67, 198.41],
+ '70.y': [124.51, 106.58],
+ '71.height': [5.57, 4.77],
+ '71.width': [2.21, 1.89],
+ '71.x': [208.4, 205.03],
+ '71.y': [125.22, 107.2],
+ '72.height': [12.45, 10.66],
+ '72.width': [7.58, 6.49],
+ '72.x': [206.61, 203.5],
+ '72.y': [126.01, 107.87],
+ '73.height': [1.3, 1.11],
+ '73.width': [1, 0.85],
+ '73.x': [206.96, 203.8],
+ '73.y': [137.6, 117.79],
+ '74.height': [9.87, 8.45],
+ '74.width': [8.9, 7.62],
+ '74.x': [193.33, 192.13],
+ '74.y': [121.77, 104.24],
+ '75.height': [1.54, 1.32],
+ '75.width': [1.24, 1.06],
+ '75.x': [205.78, 202.79],
+ '75.y': [120.27, 102.95],
+ '76.height': [3.17, 2.71],
+ '76.width': [1.12, 0.96],
+ '76.x': [209.53, 206],
+ '76.y': [93.3, 79.87],
+ '77.height': [1.34, 1.15],
+ '77.width': [1.06, 0.91],
+ '77.x': [210.15, 206.53],
+ '77.y': [92.12, 78.86],
+ '78.height': [10.29, 8.81],
+ '78.width': [5.17, 4.43],
+ '78.x': [215.96, 211.51],
+ '78.y': [127.27, 108.95],
+ '79.height': [1, 0.85],
+ '79.width': [0.44, 0.38],
+ '79.x': [210, 206.4],
+ '79.y': [93.93, 80.41],
+ '8.height': [11.39, 9.75],
+ '8.width': [32.74, 28.03],
+ '8.x': [253.22, 243.4],
+ '8.y': [114.66, 98.15],
+ '80.height': [0.55, 0.47],
+ '80.width': [2.15, 1.84],
+ '80.x': [172.94, 174.68],
+ '80.y': [108.55, 92.93],
+ '81.height': [6.08, 5.2],
+ '81.width': [2.84, 2.43],
+ '81.x': [190.39, 189.61],
+ '81.y': [89.72, 76.81],
+ '82.height': [14.75, 12.63],
+ '82.width': [14.81, 12.68],
+ '82.x': [178.78, 179.68],
+ '82.y': [89.93, 76.99],
+ '83.height': [3.5, 3],
+ '83.width': [3.06, 2.62],
+ '83.x': [210, 206.4],
+ '83.y': [93.21, 79.79],
+ '84.height': [2.8, 2.39],
+ '84.width': [3.45, 2.95],
+ '84.x': [221.93, 216.61],
+ '84.y': [99.26, 84.97],
+ '85.height': [1.23, 1.06],
+ '85.width': [0.62, 0.53],
+ '85.x': [221.33, 216.1],
+ '85.y': [99.21, 84.92],
+ '86.height': [1.26, 1.08],
+ '86.width': [1.32, 1.13],
+ '86.x': [218.34, 213.54],
+ '86.y': [96.01, 82.18],
+ '87.height': [7.1, 6.08],
+ '87.width': [7, 5.99],
+ '87.x': [212.77, 208.77],
+ '87.y': [89.69, 76.78],
+ '88.height': [7.51, 6.43],
+ '88.width': [5.59, 4.79],
+ '88.x': [222.23, 216.87],
+ '88.y': [98.98, 84.73],
+ '89.height': [1.47, 1.25],
+ '89.width': [0.87, 0.75],
+ '89.x': [304.29, 287.11],
+ '89.y': [129.17, 110.58],
+ '9.height': [31.63, 27.08],
+ '9.width': [14.16, 12.13],
+ '9.x': [132.44, 140.01],
+ '9.y': [134.61, 115.23],
+ '90.height': [3, 2.56],
+ '90.width': [3.77, 3.23],
+ '90.x': [258.27, 247.72],
+ '90.y': [108.04, 92.49],
+ '91.height': [10.86, 9.29],
+ '91.width': [5.88, 5.03],
+ '91.x': [254.71, 244.67],
+ '91.y': [103.65, 88.73],
+ '92.height': [6.47, 5.54],
+ '92.width': [5.33, 4.56],
+ '92.x': [256.67, 246.35],
+ '92.y': [102.08, 87.38],
+ '93.height': [14.02, 12],
+ '93.width': [6.36, 5.44],
+ '93.x': [251.08, 241.57],
+ '93.y': [97.42, 83.39],
+ '94.height': [11.02, 9.43],
+ '94.width': [5.13, 4.39],
+ '94.x': [258.14, 247.61],
+ '94.y': [101.39, 86.79],
+ '95.height': [5, 4.28],
+ '95.width': [4.66, 3.99],
+ '95.x': [273.96, 261.15],
+ '95.y': [84.44, 72.28],
+ '96.height': [3.76, 3.22],
+ '96.width': [2.4, 2.05],
+ '96.x': [275.29, 262.29],
+ '96.y': [88.58, 75.82],
+ '97.height': [10.98, 9.4],
+ '97.width': [22.92, 19.62],
+ '97.x': [247.82, 238.78],
+ '97.y': [74.8, 64.03],
+ '98.height': [21.49, 18.4],
+ '98.width': [20.92, 17.91],
+ '98.x': [233.81, 226.78],
+ '98.y': [91.38, 78.22],
+ '99.height': [4.51, 3.86],
+ '99.width': [3.29, 2.81],
+ '99.x': [248.06, 238.98],
+ '99.y': [98.94, 84.7],
+ },
+ },
+ style: {},
+ },
+ '../gallery/maps.stories.tsx.worldMercatorSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1105, 1174.58],
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/maps.stories.tsx.interactiveSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [235, 304.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/maps.stories.tsx.interactiveSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/maps.stories.tsx.mapStoryBase @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [235, 304.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/maps.stories.tsx.mapStoryBase @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/maps.stories.tsx.readerZoomCountySpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [235, 304.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/maps.stories.tsx.readerZoomCountySpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/sankey-tilemaps.stories.tsx.energyFlowSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [215, 284.58],
+ 'source.maxWidth': [215, 284.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/sankey-tilemaps.stories.tsx.energyFlowSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1105, 1174.58],
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/sankey-tilemaps.stories.tsx.budgetSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [215, 284.58],
+ 'source.maxWidth': [215, 284.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/sankey-tilemaps.stories.tsx.budgetSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1105, 1174.58],
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/sankey-tilemaps.stories.tsx.userJourneySpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [215, 284.58],
+ 'source.maxWidth': [215, 284.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/sankey-tilemaps.stories.tsx.userJourneySpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1105, 1174.58],
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/sankey-tilemaps.stories.tsx.linkStyleSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [215, 284.58],
+ 'source.maxWidth': [215, 284.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/sankey-tilemaps.stories.tsx.linkStyleSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1105, 1174.58],
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/sankey-tilemaps.stories.tsx.unemploymentSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ bottomAnchorY: [undefined, 313.3],
+ 'byline.maxWidth': [215, 284.58],
+ 'source.maxWidth': [215, 284.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/sankey-tilemaps.stories.tsx.unemploymentSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ bottomAnchorY: [undefined, 309.2],
+ 'byline.maxWidth': [1105, 1174.58],
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/sankey-tilemaps.stories.tsx.categoricalSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ bottomAnchorY: [undefined, 325.75],
+ 'byline.maxWidth': [215, 284.58],
+ 'source.maxWidth': [215, 284.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/sankey-tilemaps.stories.tsx.categoricalSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ bottomAnchorY: [undefined, 311.2],
+ 'byline.maxWidth': [1105, 1174.58],
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/sankey-tilemaps.stories.tsx.partialSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ bottomAnchorY: [undefined, 306.7],
+ 'byline.maxWidth': [215, 284.58],
+ 'source.maxWidth': [215, 284.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/sankey-tilemaps.stories.tsx.partialSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ bottomAnchorY: [undefined, 309.2],
+ 'byline.maxWidth': [1105, 1174.58],
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/sankey-tilemaps.stories.tsx.paletteSpecs.0 @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ bottomAnchorY: [undefined, 319.45],
+ 'source.maxWidth': [215, 284.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/sankey-tilemaps.stories.tsx.paletteSpecs.0 @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ bottomAnchorY: [undefined, 330.9],
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/sankey-tilemaps.stories.tsx.paletteSpecs.1 @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ bottomAnchorY: [undefined, 319.45],
+ 'source.maxWidth': [215, 284.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/sankey-tilemaps.stories.tsx.paletteSpecs.1 @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ bottomAnchorY: [undefined, 330.9],
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/sankey-tilemaps.stories.tsx.paletteSpecs.2 @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ bottomAnchorY: [undefined, 319.45],
+ 'source.maxWidth': [215, 284.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/sankey-tilemaps.stories.tsx.paletteSpecs.2 @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ bottomAnchorY: [undefined, 330.9],
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/sankey-tilemaps.stories.tsx.paletteSpecs.3 @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ bottomAnchorY: [undefined, 319.45],
+ 'source.maxWidth': [215, 284.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/sankey-tilemaps.stories.tsx.paletteSpecs.3 @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ bottomAnchorY: [undefined, 330.9],
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/sankey-tilemaps.stories.tsx.paletteSpecs.4 @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ bottomAnchorY: [undefined, 319.45],
+ 'source.maxWidth': [215, 284.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/sankey-tilemaps.stories.tsx.paletteSpecs.4 @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ bottomAnchorY: [undefined, 330.9],
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/sankey-tilemaps.stories.tsx.interactiveSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ bottomAnchorY: [undefined, 329.3],
+ 'source.maxWidth': [215, 284.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/sankey-tilemaps.stories.tsx.interactiveSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ bottomAnchorY: [undefined, 333.2],
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/showcase.stories.tsx.multiLineSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ area: {
+ 'rect.height': [121.3, 135.3],
+ },
+ chrome: {
+ bottomAnchorY: [312, 326],
+ bottomHeight: [72, 58],
+ 'byline.maxWidth': [223, 292.58],
+ 'byline.y': [42, 28],
+ 'source.maxWidth': [223, 292.58],
+ },
+ axes: {
+ 'x.end.y': [286, 300],
+ 'x.start.y': [286, 300],
+ 'y.end.y': [286, 300],
+ 'y.tick.0.position': [256.72, 267.34],
+ 'y.tick.1.position': [214.89, 220.69],
+ 'y.tick.2.position': [173.07, 174.03],
+ 'y.titlePosition.y': [225.35, 232.35],
+ },
+ marks: {
+ '0.height': [74.87, 83.51],
+ '0.y': [165.12, 165.17],
+ '1.height': [26.77, 29.86],
+ '1.y': [215.73, 221.62],
+ '2.height': [39.53, 44.09],
+ '2.y': [225.35, 232.35],
+ '3.height': [26.56, 29.63],
+ '3.y': [243.75, 252.88],
+ '4.height': [19.45, 21.69],
+ '4.y': [264.25, 275.74],
+ },
+ annotations: {
+ 'a0.dot.y': [200.25, 204.36],
+ 'a0.label.y': [188.25, 192.36],
+ },
+ },
+ style: {},
+ },
+ '../gallery/showcase.stories.tsx.multiLineSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/showcase.stories.tsx.fullChromeSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'footer.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/showcase.stories.tsx.fullChromeSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'footer.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/showcase.stories.tsx.stackedAreaSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/showcase.stories.tsx.stackedAreaSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/showcase.stories.tsx.tableSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 390],
+ },
+ chrome: {
+ 'byline.maxWidth': [215, 284.58],
+ 'source.maxWidth': [215, 284.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/showcase.stories.tsx.tableSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 1280],
+ },
+ chrome: {
+ 'byline.maxWidth': [1105, 1174.58],
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/showcase.stories.tsx.worldMapSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'mapSize.height': [149.3, 125.3],
+ },
+ area: {
+ 'rect.height': [149.3, 125.3],
+ },
+ chrome: {
+ bottomAnchorY: [324, 338],
+ bottomHeight: [66, 52],
+ 'byline.maxWidth': [235, 304.58],
+ 'byline.y': [42, 28],
+ 'source.maxWidth': [235, 304.58],
+ },
+ marks: {
+ '0.height': [2.55, 2.14],
+ '0.width': [302.38, 253.77],
+ '0.x': [33.81, 58.11],
+ '0.y': [92.66, 77.76],
+ '1.height': [12.37, 10.38],
+ '1.width': [9.17, 7.69],
+ '1.x': [210.08, 206.04],
+ '1.y': [75.41, 63.28],
+ '10.height': [38.34, 32.18],
+ '10.width': [13.44, 11.28],
+ '10.x': [126.23, 135.68],
+ '10.y': [94.42, 79.24],
+ '100.height': [1.71, 1.43],
+ '100.width': [2.75, 2.31],
+ '100.x': [256.94, 245.38],
+ '100.y': [42.38, 35.57],
+ '101.height': [4.33, 3.64],
+ '101.width': [7.12, 5.98],
+ '101.x': [249.47, 239.1],
+ '101.y': [40.11, 33.66],
+ '102.height': [14.3, 12],
+ '102.width': [11.85, 9.95],
+ '102.x': [233.78, 225.94],
+ '102.y': [33.11, 27.78],
+ '103.height': [9.55, 8.02],
+ '103.width': [10.4, 8.73],
+ '103.x': [232.61, 224.95],
+ '103.y': [31.73, 26.63],
+ '104.height': [4.25, 3.57],
+ '104.width': [6.34, 5.32],
+ '104.x': [236.44, 228.17],
+ '104.y': [29.26, 24.56],
+ '105.height': [3.96, 3.32],
+ '105.width': [7.23, 6.07],
+ '105.x': [237.74, 229.26],
+ '105.y': [26.98, 22.64],
+ '106.height': [7.51, 6.31],
+ '106.width': [11.9, 9.98],
+ '106.x': [224.38, 218.05],
+ '106.y': [27.51, 23.08],
+ '107.height': [15.38, 12.91],
+ '107.width': [17.82, 14.96],
+ '107.x': [218.58, 213.18],
+ '107.y': [30.5, 25.6],
+ '108.height': [5.1, 4.28],
+ '108.width': [4.69, 3.93],
+ '108.x': [212.98, 208.48],
+ '108.y': [33.01, 27.7],
+ '109.height': [2.5, 2.1],
+ '109.width': [2.71, 2.28],
+ '109.x': [217.84, 212.56],
+ '109.y': [28.98, 24.32],
+ '11.height': [21.27, 17.85],
+ '11.width': [16.27, 13.66],
+ '11.x': [195.4, 193.73],
+ '11.y': [68.25, 57.28],
+ '110.height': [9.96, 8.36],
+ '110.width': [7.29, 6.12],
+ '110.x': [192.19, 191.04],
+ '110.y': [6.1, 5.12],
+ '111.height': [4.14, 3.47],
+ '111.width': [6.4, 5.37],
+ '111.x': [201.04, 198.46],
+ '111.y': [15.4, 12.92],
+ '112.height': [6.42, 5.38],
+ '112.width': [12.78, 10.72],
+ '112.x': [200.79, 198.25],
+ '112.y': [18.65, 15.65],
+ '113.height': [5.1, 4.28],
+ '113.width': [7.21, 6.05],
+ '113.x': [194.66, 193.1],
+ '113.y': [16.5, 13.84],
+ '114.height': [2.41, 2.02],
+ '114.width': [5.32, 4.47],
+ '114.x': [191.85, 190.75],
+ '114.y': [21.58, 18.11],
+ '115.height': [2.66, 2.23],
+ '115.width': [4.58, 3.85],
+ '115.x': [196.72, 194.84],
+ '115.y': [21.96, 18.43],
+ '116.height': [2.77, 2.33],
+ '116.width': [2.74, 2.3],
+ '116.x': [204.06, 201],
+ '116.y': [22.1, 18.55],
+ '117.height': [4.27, 3.58],
+ '117.width': [7.02, 5.89],
+ '117.x': [199.71, 197.35],
+ '117.y': [22.33, 18.74],
+ '118.height': [2.06, 1.73],
+ '118.width': [3.85, 3.23],
+ '118.x': [199.11, 196.84],
+ '118.y': [15.23, 12.79],
+ '119.height': [1.91, 1.61],
+ '119.width': [4.82, 4.05],
+ '119.x': [199.03, 196.78],
+ '119.y': [13.94, 11.7],
+ '12.height': [15.75, 13.22],
+ '12.width': [8.28, 6.95],
+ '12.x': [220.06, 214.42],
+ '12.y': [60.5, 50.77],
+ '120.height': [1.68, 1.41],
+ '120.width': [3.09, 2.6],
+ '120.x': [200.17, 197.73],
+ '120.y': [12.66, 10.62],
+ '121.height': [6.79, 5.7],
+ '121.width': [6.34, 5.32],
+ '121.x': [189.16, 188.49],
+ '121.y': [16.38, 13.75],
+ '122.height': [2.92, 2.45],
+ '122.width': [4.58, 3.85],
+ '122.x': [201.56, 198.9],
+ '122.y': [26.07, 21.88],
+ '123.height': [6.97, 5.85],
+ '123.width': [5.18, 4.34],
+ '123.x': [200.32, 197.86],
+ '123.y': [28.41, 23.84],
+ '124.height': [6.35, 5.33],
+ '124.width': [14.97, 12.56],
+ '124.x': [204.58, 201.43],
+ '124.y': [28.1, 23.58],
+ '125.height': [3.02, 2.54],
+ '125.width': [1.43, 1.2],
+ '125.x': [199.44, 197.12],
+ '125.y': [27.57, 23.14],
+ '126.height': [3.85, 3.23],
+ '126.width': [4.2, 3.53],
+ '126.x': [195, 193.39],
+ '126.y': [23.92, 20.08],
+ '127.height': [1.92, 1.61],
+ '127.width': [3.18, 2.67],
+ '127.x': [189.37, 188.67],
+ '127.y': [22.69, 19.04],
+ '128.height': [0.62, 0.52],
+ '128.width': [0.39, 0.33],
+ '128.x': [189.02, 188.38],
+ '128.y': [20.6, 17.29],
+ '129.height': [1.74, 1.46],
+ '129.width': [2.56, 2.15],
+ '129.x': [186.76, 186.48],
+ '129.y': [19.4, 16.28],
+ '13.height': [11.73, 9.85],
+ '13.width': [6.78, 5.69],
+ '13.x': [214.01, 209.35],
+ '13.y': [67.97, 57.04],
+ '130.height': [2.36, 1.98],
+ '130.width': [2.56, 2.15],
+ '130.x': [187.31, 186.94],
+ '130.y': [17.63, 14.8],
+ '131.height': [5.44, 4.57],
+ '131.width': [2.48, 2.08],
+ '131.x': [177.72, 178.89],
+ '131.y': [27.96, 23.47],
+ '132.height': [7.78, 6.53],
+ '132.width': [9.27, 7.78],
+ '132.x': [178.01, 179.14],
+ '132.y': [26.54, 22.27],
+ '133.height': [2.97, 2.49],
+ '133.width': [2.79, 2.34],
+ '133.x': [178.06, 179.18],
+ '133.y': [16.26, 13.65],
+ '134.height': [2.55, 2.14],
+ '134.width': [1.7, 1.43],
+ '134.x': [321.27, 299.37],
+ '134.y': [97.24, 81.61],
+ '135.height': [4.85, 4.07],
+ '135.width': [4.32, 3.62],
+ '135.x': [318.5, 297.04],
+ '135.y': [81.91, 68.74],
+ '136.height': [12.07, 10.13],
+ '136.width': [16.14, 13.55],
+ '136.x': [306.19, 286.71],
+ '136.y': [112.75, 94.63],
+ '137.height': [35.4, 29.71],
+ '137.width': [34.59, 29.03],
+ '137.x': [275.22, 260.72],
+ '137.y': [86.57, 72.66],
+ '138.height': [4.42, 3.71],
+ '138.width': [1.84, 1.54],
+ '138.x': [252.88, 241.97],
+ '138.y': [63.01, 52.88],
+ '139.height': [35.83, 30.07],
+ '139.width': [42.15, 35.37],
+ '139.x': [241.04, 232.03],
+ '139.y': [17.68, 14.84],
+ '14.height': [15.57, 13.07],
+ '14.width': [13.55, 11.37],
+ '14.x': [203.56, 200.58],
+ '14.y': [49.27, 41.35],
+ '140.height': [3.66, 3.07],
+ '140.width': [1.14, 0.96],
+ '140.x': [283.69, 267.82],
+ '140.y': [45.64, 38.31],
+ '141.height': [10.28, 8.62],
+ '141.width': [9.06, 7.6],
+ '141.x': [189.95, 189.15],
+ '141.y': [23.35, 19.6],
+ '142.height': [2.4, 2.02],
+ '142.width': [3.14, 2.63],
+ '142.x': [190.39, 189.53],
+ '142.y': [14.14, 11.86],
+ '143.height': [7.33, 6.15],
+ '143.width': [6.33, 5.31],
+ '143.x': [179.84, 180.67],
+ '143.y': [13.42, 11.26],
+ '144.height': [2.06, 1.73],
+ '144.width': [6.48, 5.44],
+ '144.x': [170.19, 172.57],
+ '144.y': [7.72, 6.48],
+ '145.height': [3.58, 3],
+ '145.width': [4.32, 3.62],
+ '145.x': [218.85, 213.41],
+ '145.y': [28.38, 23.81],
+ '146.height': [2.43, 2.04],
+ '146.width': [5.48, 4.6],
+ '146.x': [214.64, 209.87],
+ '146.y': [26.73, 22.43],
+ '147.height': [14.71, 12.35],
+ '147.width': [8.12, 6.81],
+ '147.x': [284.79, 268.75],
+ '147.y': [53.17, 44.62],
+ '148.height': [7.09, 5.95],
+ '148.width': [16.39, 13.75],
+ '148.x': [270.41, 256.68],
+ '148.y': [66.33, 55.67],
+ '149.height': [1.66, 1.39],
+ '149.width': [1, 0.84],
+ '149.x': [282.61, 266.92],
+ '149.y': [68.03, 57.1],
+ '15.height': [18.05, 15.15],
+ '15.width': [8.67, 7.27],
+ '15.x': [196.42, 194.58],
+ '15.y': [47.71, 40.04],
+ '150.height': [1.32, 1.1],
+ '150.width': [2.05, 1.72],
+ '150.x': [194.97, 193.37],
+ '150.y': [23.6, 19.8],
+ '151.height': [7, 5.87],
+ '151.width': [7.63, 6.41],
+ '151.x': [197.11, 195.16],
+ '151.y': [5.48, 4.6],
+ '152.height': [1.66, 1.39],
+ '152.width': [3.98, 3.34],
+ '152.x': [197.06, 195.12],
+ '152.y': [21.1, 17.71],
+ '153.height': [2.31, 1.94],
+ '153.width': [4.74, 3.98],
+ '153.x': [193.62, 192.24],
+ '153.y': [19.72, 16.55],
+ '154.height': [6.27, 5.26],
+ '154.width': [5.85, 4.91],
+ '154.x': [215.6, 210.68],
+ '154.y': [53.74, 45.1],
+ '155.height': [14.64, 12.29],
+ '155.width': [7.48, 6.28],
+ '155.x': [286.98, 270.58],
+ '155.y': [24.82, 20.83],
+ '156.height': [9.04, 7.58],
+ '156.width': [7.29, 6.12],
+ '156.x': [133.26, 141.58],
+ '156.y': [96.39, 80.9],
+ '157.height': [7.25, 6.08],
+ '157.width': [8.69, 7.29],
+ '157.x': [220.86, 215.09],
+ '157.y': [52.61, 44.15],
+ '158.height': [17.33, 14.54],
+ '158.width': [18.02, 15.12],
+ '158.x': [212.96, 208.47],
+ '158.y': [38.27, 32.11],
+ '159.height': [10.62, 8.91],
+ '159.width': [190.07, 159.52],
+ '159.x': [92.62, 107.47],
+ '159.y': [138.68, 116.39],
+ '16.height': [2.11, 1.77],
+ '16.width': [2.52, 2.12],
+ '16.x': [122.82, 132.82],
+ '16.y': [51.59, 43.3],
+ '160.height': [0.69, 0.58],
+ '160.width': [1.36, 1.14],
+ '160.x': [210.55, 206.44],
+ '160.y': [34.6, 29.04],
+ '161.height': [0.62, 0.52],
+ '161.width': [1.38, 1.16],
+ '161.x': [210.18, 206.13],
+ '161.y': [35.12, 29.47],
+ '162.height': [15.4, 12.92],
+ '162.width': [13.2, 11.07],
+ '162.x': [170.92, 173.18],
+ '162.y': [34.51, 28.97],
+ '163.height': [10.4, 8.73],
+ '163.width': [10.69, 8.98],
+ '163.x': [204.76, 201.58],
+ '163.y': [38.87, 32.62],
+ '164.height': [14.72, 12.36],
+ '164.width': [13.2, 11.08],
+ '164.x': [192.58, 191.36],
+ '164.y': [37.24, 31.25],
+ '165.height': [13.2, 11.08],
+ '165.width': [12.63, 10.6],
+ '165.x': [213.08, 208.57],
+ '165.y': [57.16, 47.97],
+ '166.height': [2.02, 1.7],
+ '166.width': [1.35, 1.14],
+ '166.x': [220.31, 214.63],
+ '166.y': [59.73, 50.13],
+ '167.height': [3.97, 3.33],
+ '167.width': [5.48, 4.6],
+ '167.x': [221.13, 215.32],
+ '167.y': [61.14, 51.31],
+ '168.height': [6.56, 5.51],
+ '168.width': [4.67, 3.92],
+ '168.x': [210.32, 206.25],
+ '168.y': [69.41, 58.25],
+ '169.height': [2.05, 1.72],
+ '169.width': [1.54, 1.29],
+ '169.x': [209.83, 205.84],
+ '169.y': [75.62, 63.46],
+ '17.height': [2.56, 2.15],
+ '17.width': [3.02, 2.54],
+ '17.x': [124.96, 134.61],
+ '17.y': [51.62, 43.32],
+ '170.height': [2.49, 2.09],
+ '170.width': [2.91, 2.44],
+ '170.x': [196.57, 194.71],
+ '170.y': [25.12, 21.08],
+ '171.height': [1.45, 1.22],
+ '171.width': [1.89, 1.59],
+ '171.x': [200.38, 197.9],
+ '171.y': [27.93, 23.44],
+ '172.height': [3.77, 3.16],
+ '172.width': [3.35, 2.81],
+ '172.x': [198.72, 196.52],
+ '172.y': [24.23, 20.34],
+ '173.height': [1.6, 1.34],
+ '173.width': [1.37, 1.15],
+ '173.x': [198.77, 196.56],
+ '173.y': [26.76, 22.46],
+ '174.height': [1.39, 1.17],
+ '174.width': [1.26, 1.06],
+ '174.x': [199.97, 197.57],
+ '174.y': [27, 22.66],
+ '175.height': [1.02, 0.85],
+ '175.width': [0.96, 0.8],
+ '175.x': [132.36, 140.83],
+ '175.y': [61.79, 51.86],
+ '176.height': [10.02, 8.41],
+ '176.width': [9.81, 8.23],
+ '176.x': [205.34, 202.07],
+ '176.y': [60.24, 50.56],
+ '18.height': [28.48, 23.9],
+ '18.width': [223.2, 187.32],
+ '18.x': [74.72, 92.45],
+ '18.y': [0.6, 0.5],
+ '19.height': [3.64, 3.05],
+ '19.width': [1.64, 1.37],
+ '19.x': [120.81, 131.13],
+ '19.y': [43.74, 36.71],
+ '2.height': [7.3, 6.13],
+ '2.width': [7.13, 5.98],
+ '2.x': [170.86, 173.13],
+ '2.y': [43.08, 36.15],
+ '20.height': [1.05, 0.88],
+ '20.width': [2.3, 1.93],
+ '20.x': [142.45, 149.29],
+ '20.y': [128.89, 108.17],
+ '21.height': [13.08, 10.98],
+ '21.width': [15, 12.59],
+ '21.x': [188.16, 187.65],
+ '21.y': [0.77, 0.65],
+ '22.height': [12.33, 10.35],
+ '22.width': [32.95, 27.65],
+ '22.x': [145.66, 151.99],
+ '23.height': [1.04, 0.88],
+ '23.width': [1.55, 1.3],
+ '23.x': [233.63, 225.81],
+ '23.y': [126.66, 106.3],
+ '24.height': [1.28, 1.08],
+ '24.width': [2.08, 1.75],
+ '24.x': [291.35, 274.26],
+ '24.y': [83.83, 70.36],
+ '25.height': [13.68, 11.48],
+ '25.width': [13.48, 11.32],
+ '25.x': [198.17, 196.05],
+ '25.y': [99.45, 83.46],
+ '26.height': [2.14, 1.79],
+ '26.width': [1.93, 1.62],
+ '26.x': [206.63, 203.15],
+ '26.y': [106.61, 89.48],
+ '27.height': [19.96, 16.75],
+ '27.width': [21.17, 17.77],
+ '27.x': [91.96, 106.92],
+ '27.y': [37.68, 31.62],
+ '28.height': [5.09, 4.27],
+ '28.width': [4.16, 3.49],
+ '28.x': [138.88, 146.29],
+ '28.y': [108.18, 90.79],
+ '29.height': [43.77, 36.74],
+ '29.width': [33.46, 28.08],
+ '29.x': [121.93, 132.07],
+ '29.y': [68.27, 57.29],
+ '3.height': [28.47, 23.89],
+ '3.width': [59.19, 49.68],
+ '3.x': [93.72, 108.39],
+ '3.y': [0.09, 0.08],
+ '30.height': [14.78, 12.4],
+ '30.width': [10.96, 9.2],
+ '30.x': [126, 135.49],
+ '30.y': [85.54, 71.79],
+ '31.height': [20.9, 17.54],
+ '31.width': [11.71, 9.82],
+ '31.x': [115.43, 126.61],
+ '31.y': [74.38, 62.42],
+ '32.height': [19.24, 16.15],
+ '32.width': [10.36, 8.7],
+ '32.x': [117.4, 128.27],
+ '32.y': [60.03, 50.38],
+ '33.height': [2.74, 2.3],
+ '33.width': [4.85, 4.07],
+ '33.x': [114.33, 125.69],
+ '33.y': [63.25, 53.09],
+ '34.height': [3.42, 2.87],
+ '34.width': [2.74, 2.3],
+ '34.x': [112.07, 123.79],
+ '34.y': [61.42, 51.55],
+ '35.height': [4.88, 4.1],
+ '35.width': [4.12, 3.46],
+ '35.x': [110.87, 122.78],
+ '35.y': [57.1, 47.92],
+ '36.height': [3.43, 2.87],
+ '36.width': [5.32, 4.46],
+ '36.x': [109.67, 121.78],
+ '36.y': [55.98, 46.98],
+ '37.height': [1.45, 1.22],
+ '37.width': [2.01, 1.69],
+ '37.x': [108.94, 121.16],
+ '37.y': [57.77, 48.48],
+ '38.height': [4.62, 3.87],
+ '38.width': [3.57, 3],
+ '38.x': [107.27, 119.76],
+ '38.y': [53.94, 45.27],
+ '39.height': [2.94, 2.47],
+ '39.width': [1.4, 1.18],
+ '39.x': [110.02, 122.07],
+ '39.y': [53.17, 44.62],
+ '4.height': [47.9, 40.2],
+ '4.width': [82.99, 69.65],
+ '4.x': [53.03, 74.24],
+ '4.y': [4.81, 4.03],
+ '40.height': [13.13, 11.02],
+ '40.width': [11.47, 9.63],
+ '40.x': [122.64, 132.66],
+ '40.y': [60.34, 50.64],
+ '41.height': [8.17, 6.86],
+ '41.width': [4.05, 3.4],
+ '41.x': [132.57, 141],
+ '41.y': [64.68, 54.28],
+ '42.height': [4.85, 4.07],
+ '42.width': [3.55, 2.98],
+ '42.x': [135.38, 143.35],
+ '42.y': [67.37, 56.54],
+ '43.height': [52.25, 43.85],
+ '43.width': [53.8, 45.16],
+ '43.x': [138.35, 145.85],
+ '43.y': [19.69, 16.52],
+ '44.height': [7.31, 6.13],
+ '44.width': [4.88, 4.1],
+ '44.x': [115.72, 126.86],
+ '44.y': [72.72, 61.03],
+ '45.height': [0.64, 0.54],
+ '45.width': [1.37, 1.15],
+ '45.x': [128.83, 137.86],
+ '45.y': [53.15, 44.61],
+ '46.height': [0.92, 0.78],
+ '46.width': [1.73, 1.45],
+ '46.x': [119.56, 130.08],
+ '46.y': [53.14, 44.6],
+ '47.height': [3.7, 3.1],
+ '47.width': [8.61, 7.23],
+ '47.x': [114.79, 126.07],
+ '47.y': [47.96, 40.25],
+ '48.height': [7.57, 6.36],
+ '48.width': [6.42, 5.38],
+ '48.x': [206.13, 202.73],
+ '48.y': [92.08, 77.28],
+ '49.height': [10.14, 8.51],
+ '49.width': [8.03, 6.74],
+ '49.x': [201.28, 198.66],
+ '49.y': [94.51, 79.32],
+ '5.height': [13.51, 11.34],
+ '5.width': [28.89, 24.25],
+ '5.x': [218.18, 212.85],
+ '5.y': [16.05, 13.47],
+ '50.height': [13.34, 11.2],
+ '50.width': [11.16, 9.37],
+ '50.x': [194.83, 193.25],
+ '50.y': [93.7, 78.64],
+ '51.height': [4.84, 4.06],
+ '51.width': [5.15, 4.32],
+ '51.x': [170.15, 172.54],
+ '51.y': [55.31, 46.42],
+ '52.height': [16.7, 14.02],
+ '52.width': [13.84, 11.61],
+ '52.x': [174.74, 176.39],
+ '52.y': [45.99, 38.6],
+ '53.height': [14.19, 11.91],
+ '53.width': [10.11, 8.49],
+ '53.x': [170.86, 173.13],
+ '53.y': [43.36, 36.39],
+ '54.height': [6.98, 5.86],
+ '54.width': [2.57, 2.15],
+ '54.x': [185.66, 185.55],
+ '54.y': [60.26, 50.57],
+ '55.height': [13.27, 11.14],
+ '55.width': [12.95, 10.87],
+ '55.x': [185.25, 185.21],
+ '55.y': [47.64, 39.99],
+ '56.height': [11.02, 9.25],
+ '56.width': [10.05, 8.43],
+ '56.x': [187.3, 186.93],
+ '56.y': [58.4, 49.01],
+ '57.height': [12.77, 10.72],
+ '57.width': [6.45, 5.41],
+ '57.x': [192.25, 191.09],
+ '57.y': [59.55, 49.97],
+ '58.height': [5.83, 4.9],
+ '58.width': [1.63, 1.37],
+ '58.y': [61.64, 51.74],
+ '59.height': [7.33, 6.15],
+ '59.width': [3.68, 3.08],
+ '59.x': [182.23, 182.68],
+ '59.y': [61.55, 51.66],
+ '6.height': [8.31, 6.97],
+ '6.width': [14.09, 11.82],
+ '6.x': [226.03, 219.43],
+ '6.y': [24.79, 20.8],
+ '60.height': [7.1, 5.96],
+ '60.width': [5.16, 4.33],
+ '60.x': [177.66, 178.84],
+ '60.y': [62.21, 52.21],
+ '61.height': [6.04, 5.07],
+ '61.width': [6.17, 5.18],
+ '61.x': [172.16, 174.23],
+ '61.y': [59.86, 50.24],
+ '62.height': [1.81, 1.52],
+ '62.width': [2.52, 2.12],
+ '62.x': [170.88, 173.15],
+ '62.y': [59.81, 50.19],
+ '63.height': [4.81, 4.04],
+ '63.width': [3.32, 2.78],
+ '63.x': [175.24, 176.81],
+ '63.y': [64.48, 54.11],
+ '64.height': [3.74, 3.14],
+ '64.width': [2.56, 2.15],
+ '64.x': [173.73, 175.54],
+ '64.y': [62.75, 52.67],
+ '65.height': [6.27, 5.26],
+ '65.width': [6.48, 5.44],
+ '65.x': [180.36, 181.1],
+ '65.y': [56.98, 47.82],
+ '66.height': [10.19, 8.55],
+ '66.width': [11.03, 9.26],
+ '66.x': [197.35, 195.36],
+ '66.y': [61.5, 51.62],
+ '67.height': [10.1, 8.48],
+ '67.width': [6.3, 5.28],
+ '67.x': [194.48, 192.96],
+ '67.y': [70.01, 58.76],
+ '68.height': [7.27, 6.1],
+ '68.width': [4.82, 4.04],
+ '68.x': [192.53, 191.32],
+ '68.y': [71.63, 60.11],
+ '69.height': [1.47, 1.23],
+ '69.width': [1.69, 1.42],
+ '69.x': [192.96, 191.68],
+ '69.y': [71.68, 60.15],
+ '7.height': [9.37, 7.86],
+ '7.width': [13.14, 11.03],
+ '7.x': [304.99, 285.7],
+ '7.y': [77.19, 64.78],
+ '70.height': [11.05, 9.27],
+ '70.width': [10.05, 8.43],
+ '70.x': [203.38, 200.43],
+ '70.y': [83.79, 70.32],
+ '71.height': [8.61, 7.22],
+ '71.width': [2.48, 2.08],
+ '71.x': [212.6, 208.16],
+ '71.y': [84.93, 71.28],
+ '72.height': [18.38, 15.43],
+ '72.width': [8.93, 7.5],
+ '72.x': [210.42, 206.34],
+ '72.y': [86.17, 72.32],
+ '73.height': [1.77, 1.48],
+ '73.width': [1.13, 0.95],
+ '73.x': [209.91, 205.91],
+ '73.y': [103.38, 86.76],
+ '74.height': [15.38, 12.91],
+ '74.width': [10.62, 8.92],
+ '74.x': [194.76, 193.19],
+ '74.y': [79.42, 66.66],
+ '75.height': [2.48, 2.08],
+ '75.width': [1.47, 1.24],
+ '75.x': [209.83, 205.84],
+ '75.y': [77.02, 64.64],
+ '76.height': [4, 3.36],
+ '76.width': [1.02, 0.86],
+ '76.x': [212.28, 207.9],
+ '76.y': [37.09, 31.13],
+ '77.height': [1.62, 1.36],
+ '77.width': [1.01, 0.84],
+ '77.x': [212.71, 208.26],
+ '77.y': [35.67, 29.93],
+ '78.height': [15.17, 12.73],
+ '78.width': [6.77, 5.68],
+ '78.x': [220.71, 214.97],
+ '78.y': [88.14, 73.97],
+ '79.height': [1.25, 1.05],
+ '79.width': [0.46, 0.39],
+ '79.x': [212.76, 208.3],
+ '79.y': [37.87, 31.78],
+ '8.height': [18.23, 15.3],
+ '8.width': [39.24, 32.93],
+ '8.x': [266.39, 253.31],
+ '8.y': [68, 57.07],
+ '80.height': [0.85, 0.71],
+ '80.width': [2.54, 2.13],
+ '80.x': [170.76, 173.05],
+ '80.y': [58.39, 49],
+ '81.height': [7.34, 6.16],
+ '81.width': [3.16, 2.65],
+ '81.x': [190.91, 189.96],
+ '81.y': [32.89, 27.6],
+ '82.height': [19.43, 16.3],
+ '82.width': [16.9, 14.18],
+ '82.x': [177.97, 179.1],
+ '82.y': [33.12, 27.8],
+ '83.height': [4.43, 3.72],
+ '83.width': [3.04, 2.55],
+ '83.x': [213.02, 208.52],
+ '83.y': [36.98, 31.04],
+ '84.height': [3.91, 3.28],
+ '84.width': [3.83, 3.22],
+ '84.x': [227.28, 220.48],
+ '84.y': [44.81, 37.61],
+ '85.height': [1.7, 1.43],
+ '85.width': [0.75, 0.63],
+ '85.x': [226.4, 219.75],
+ '85.y': [44.75, 37.56],
+ '86.height': [1.64, 1.38],
+ '86.width': [1.58, 1.32],
+ '86.x': [222.43, 216.42],
+ '86.y': [40.5, 33.99],
+ '87.height': [8.67, 7.28],
+ '87.width': [8.34, 7],
+ '87.x': [215.56, 210.64],
+ '87.y': [32.85, 27.57],
+ '88.height': [10.81, 9.07],
+ '88.width': [6, 5.04],
+ '88.x': [228.35, 221.38],
+ '88.y': [44.44, 37.3],
+ '89.height': [2.23, 1.87],
+ '89.width': [0.65, 0.55],
+ '89.x': [325.2, 302.66],
+ '89.y': [91.08, 76.44],
+ '9.height': [33.29, 27.94],
+ '9.width': [12.69, 10.65],
+ '9.x': [128.78, 137.82],
+ '9.y': [99.16, 83.22],
+ '90.height': [4.65, 3.9],
+ '90.width': [4.53, 3.8],
+ '90.x': [271.46, 257.57],
+ '90.y': [57.6, 48.34],
+ '91.height': [16.73, 14.04],
+ '91.width': [7.62, 6.4],
+ '91.x': [266.3, 253.23],
+ '91.y': [51.03, 42.82],
+ '92.height': [9.63, 8.08],
+ '92.width': [7.46, 6.26],
+ '92.x': [268.11, 254.75],
+ '92.y': [48.76, 40.92],
+ '93.height': [20.54, 17.24],
+ '93.width': [7.99, 6.71],
+ '93.x': [261.37, 249.1],
+ '93.y': [42.34, 35.54],
+ '94.height': [16.64, 13.96],
+ '94.width': [8.28, 6.95],
+ '94.x': [269.27, 255.72],
+ '94.y': [47.77, 40.09],
+ '95.height': [5.28, 4.43],
+ '95.width': [3.92, 3.29],
+ '95.x': [279.31, 264.15],
+ '95.y': [27.28, 22.89],
+ '96.height': [4.32, 3.63],
+ '96.width': [3.75, 3.15],
+ '96.x': [282.03, 266.43],
+ '96.y': [31.61, 26.53],
+ '97.height': [9.74, 8.17],
+ '97.width': [24.39, 20.47],
+ '97.x': [247.31, 237.29],
+ '97.y': [18.9, 15.86],
+ '98.height': [30.35, 25.47],
+ '98.width': [22.76, 19.1],
+ '98.x': [240.99, 231.99],
+ '98.y': [34.79, 29.2],
+ '99.height': [6.36, 5.34],
+ '99.width': [4.81, 4.04],
+ '99.x': [256.89, 245.33],
+ '99.y': [44.39, 37.25],
+ },
+ },
+ style: {},
+ },
+ '../gallery/showcase.stories.tsx.worldMapSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1105, 1174.58],
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/showcase.stories.tsx.sankeySpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [215, 284.58],
+ 'source.maxWidth': [215, 284.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/showcase.stories.tsx.sankeySpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1105, 1174.58],
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/showcase.stories.tsx.scatterSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/showcase.stories.tsx.scatterSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/showcase.stories.tsx.keyboardSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/showcase.stories.tsx.keyboardSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/tables.stories.tsx.basicSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 390],
+ },
+ chrome: {
+ 'source.maxWidth': [215, 284.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/tables.stories.tsx.basicSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 1280],
+ },
+ chrome: {
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/tables.stories.tsx.heatmapSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 390],
+ },
+ chrome: {
+ 'source.maxWidth': [215, 284.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/tables.stories.tsx.heatmapSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 1280],
+ },
+ chrome: {
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/tables.stories.tsx.inlineBarSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 390],
+ },
+ chrome: {
+ 'source.maxWidth': [215, 284.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/tables.stories.tsx.inlineBarSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 1280],
+ },
+ chrome: {
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/tables.stories.tsx.sparklineSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 390],
+ },
+ chrome: {
+ 'source.maxWidth': [215, 284.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/tables.stories.tsx.sparklineSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 1280],
+ },
+ chrome: {
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/tables.stories.tsx.flagsSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 390],
+ },
+ chrome: {
+ 'source.maxWidth': [215, 284.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/tables.stories.tsx.flagsSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 1280],
+ },
+ chrome: {
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/tables.stories.tsx.imageSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 390],
+ },
+ chrome: {
+ 'source.maxWidth': [215, 284.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/tables.stories.tsx.imageSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 1280],
+ },
+ chrome: {
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/tables.stories.tsx.categorySpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 390],
+ },
+ chrome: {
+ 'source.maxWidth': [215, 284.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/tables.stories.tsx.categorySpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 1280],
+ },
+ chrome: {
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/tables.stories.tsx.builtinSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 390],
+ },
+ chrome: {
+ 'source.maxWidth': [215, 284.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/tables.stories.tsx.builtinSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 1280],
+ },
+ chrome: {
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/tables.stories.tsx.controlledSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 390],
+ },
+ chrome: {
+ 'source.maxWidth': [215, 284.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/tables.stories.tsx.controlledSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 1280],
+ },
+ chrome: {
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/tables.stories.tsx.deltaSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 390],
+ },
+ chrome: {
+ 'source.maxWidth': [215, 284.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/tables.stories.tsx.deltaSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 1280],
+ },
+ chrome: {
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/tables.stories.tsx.totalsSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 390],
+ },
+ chrome: {
+ 'source.maxWidth': [215, 284.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/tables.stories.tsx.totalsSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 1280],
+ },
+ chrome: {
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/tables.stories.tsx.relaxedSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 390],
+ },
+ chrome: {
+ 'source.maxWidth': [215, 284.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/tables.stories.tsx.relaxedSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 1280],
+ },
+ chrome: {
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/welcome.stories.tsx.heroSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ area: {
+ 'rect.height': [138.7, 152.7],
+ },
+ chrome: {
+ bottomAnchorY: [312, 326],
+ bottomHeight: [72, 58],
+ 'byline.maxWidth': [223, 292.58],
+ 'byline.y': [42, 28],
+ 'source.maxWidth': [223, 292.58],
+ },
+ axes: {
+ 'x.end.y': [286, 300],
+ 'x.start.y': [286, 300],
+ 'y.end.y': [286, 300],
+ 'y.tick.0.position': [252.52, 263.14],
+ 'y.tick.1.position': [204.69, 210.49],
+ 'y.tick.2.position': [156.87, 157.83],
+ 'y.titlePosition.y': [216.65, 223.65],
+ },
+ marks: {
+ '0.height': [85.61, 94.25],
+ '0.y': [147.78, 147.83],
+ '1.height': [30.61, 33.7],
+ '1.y': [205.65, 211.54],
+ '2.height': [45.2, 49.76],
+ '2.y': [216.65, 223.65],
+ '3.height': [30.37, 33.44],
+ '3.y': [237.69, 246.82],
+ '4.height': [22.24, 24.48],
+ '4.y': [261.13, 272.62],
+ },
+ annotations: {
+ 'a0.connector.from.y': [140.44, 140.49],
+ 'a0.connector.to.y': [146.05, 146.1],
+ 'a0.dot.y': [147.78, 147.83],
+ 'a0.label.y': [123.78, 123.83],
+ },
+ },
+ style: {},
+ },
+ '../gallery/welcome.stories.tsx.heroSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/welcome.stories.tsx.quickStartSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../gallery/welcome.stories.tsx.quickStartSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/a11y.stories.tsx.patternStackedSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/a11y.stories.tsx.patternStackedSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-bars.stories.tsx.simpleColumnSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-bars.stories.tsx.simpleColumnSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-bars.stories.tsx.longLabelsSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-bars.stories.tsx.longLabelsSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-bars.stories.tsx.simpleBarSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-bars.stories.tsx.simpleBarSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-bars.stories.tsx.energyColumnSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-bars.stories.tsx.energyColumnSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-bars.stories.tsx.tempSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-bars.stories.tsx.tempSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-bars.stories.tsx.populationBarSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-bars.stories.tsx.populationBarSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-bars.stories.tsx.compactPopSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-bars.stories.tsx.compactPopSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-beeswarm.stories.tsx.beeswarmBasicSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-beeswarm.stories.tsx.beeswarmBasicSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-beeswarm.stories.tsx.beeswarmGroupedSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-beeswarm.stories.tsx.beeswarmGroupedSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-beeswarm.stories.tsx.beeswarmSizedSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-beeswarm.stories.tsx.beeswarmSizedSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-calendar.stories.tsx.calendarDivergingSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-calendar.stories.tsx.calendarDivergingSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-calendar.stories.tsx.calendarTwoYearSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ area: {
+ 'rect.height': [203.3, 201.7],
+ },
+ chrome: {
+ bottomAnchorY: [331, 329.4],
+ bottomHeight: [53, 54.6],
+ 'source.maxWidth': [223, 292.58],
+ },
+ marks: {
+ '0.y': [213.35, 212.55],
+ '1.y': [159.35, 158.55],
+ '10.y': [177.35, 176.55],
+ '100.y': [168.35, 167.55],
+ '101.y': [177.35, 176.55],
+ '102.y': [186.35, 185.55],
+ '103.y': [195.35, 194.55],
+ '104.y': [204.35, 203.55],
+ '105.y': [213.35, 212.55],
+ '106.y': [159.35, 158.55],
+ '107.y': [168.35, 167.55],
+ '108.y': [177.35, 176.55],
+ '109.y': [186.35, 185.55],
+ '11.y': [186.35, 185.55],
+ '110.y': [195.35, 194.55],
+ '111.y': [204.35, 203.55],
+ '112.y': [213.35, 212.55],
+ '113.y': [159.35, 158.55],
+ '114.y': [168.35, 167.55],
+ '115.y': [177.35, 176.55],
+ '116.y': [186.35, 185.55],
+ '117.y': [195.35, 194.55],
+ '118.y': [204.35, 203.55],
+ '119.y': [213.35, 212.55],
+ '12.y': [195.35, 194.55],
+ '120.y': [159.35, 158.55],
+ '121.y': [168.35, 167.55],
+ '122.y': [177.35, 176.55],
+ '123.y': [186.35, 185.55],
+ '124.y': [195.35, 194.55],
+ '125.y': [204.35, 203.55],
+ '126.y': [213.35, 212.55],
+ '127.y': [159.35, 158.55],
+ '128.y': [168.35, 167.55],
+ '129.y': [177.35, 176.55],
+ '13.y': [204.35, 203.55],
+ '130.y': [186.35, 185.55],
+ '131.y': [195.35, 194.55],
+ '132.y': [204.35, 203.55],
+ '133.y': [213.35, 212.55],
+ '134.y': [159.35, 158.55],
+ '135.y': [168.35, 167.55],
+ '136.y': [177.35, 176.55],
+ '137.y': [186.35, 185.55],
+ '138.y': [195.35, 194.55],
+ '139.y': [204.35, 203.55],
+ '14.y': [213.35, 212.55],
+ '140.y': [213.35, 212.55],
+ '141.y': [159.35, 158.55],
+ '142.y': [168.35, 167.55],
+ '143.y': [177.35, 176.55],
+ '144.y': [186.35, 185.55],
+ '145.y': [195.35, 194.55],
+ '146.y': [204.35, 203.55],
+ '147.y': [213.35, 212.55],
+ '148.y': [159.35, 158.55],
+ '149.y': [168.35, 167.55],
+ '15.y': [159.35, 158.55],
+ '150.y': [177.35, 176.55],
+ '151.y': [186.35, 185.55],
+ '152.y': [195.35, 194.55],
+ '153.y': [204.35, 203.55],
+ '154.y': [213.35, 212.55],
+ '155.y': [159.35, 158.55],
+ '156.y': [168.35, 167.55],
+ '157.y': [177.35, 176.55],
+ '158.y': [186.35, 185.55],
+ '159.y': [195.35, 194.55],
+ '16.y': [168.35, 167.55],
+ '160.y': [204.35, 203.55],
+ '161.y': [213.35, 212.55],
+ '162.y': [159.35, 158.55],
+ '163.y': [168.35, 167.55],
+ '164.y': [177.35, 176.55],
+ '165.y': [186.35, 185.55],
+ '166.y': [195.35, 194.55],
+ '167.y': [204.35, 203.55],
+ '168.y': [213.35, 212.55],
+ '169.y': [159.35, 158.55],
+ '17.y': [177.35, 176.55],
+ '170.y': [168.35, 167.55],
+ '171.y': [177.35, 176.55],
+ '172.y': [186.35, 185.55],
+ '173.y': [195.35, 194.55],
+ '174.y': [204.35, 203.55],
+ '175.y': [213.35, 212.55],
+ '176.y': [159.35, 158.55],
+ '177.y': [168.35, 167.55],
+ '178.y': [177.35, 176.55],
+ '179.y': [186.35, 185.55],
+ '18.y': [186.35, 185.55],
+ '180.y': [195.35, 194.55],
+ '181.y': [204.35, 203.55],
+ '182.y': [213.35, 212.55],
+ '183.y': [159.35, 158.55],
+ '184.y': [168.35, 167.55],
+ '185.y': [177.35, 176.55],
+ '186.y': [186.35, 185.55],
+ '187.y': [195.35, 194.55],
+ '188.y': [204.35, 203.55],
+ '189.y': [213.35, 212.55],
+ '19.y': [195.35, 194.55],
+ '190.y': [159.35, 158.55],
+ '191.y': [168.35, 167.55],
+ '192.y': [177.35, 176.55],
+ '193.y': [186.35, 185.55],
+ '194.y': [195.35, 194.55],
+ '195.y': [204.35, 203.55],
+ '196.y': [213.35, 212.55],
+ '197.y': [159.35, 158.55],
+ '198.y': [168.35, 167.55],
+ '199.y': [177.35, 176.55],
+ '2.y': [168.35, 167.55],
+ '20.y': [204.35, 203.55],
+ '200.y': [186.35, 185.55],
+ '201.y': [195.35, 194.55],
+ '202.y': [204.35, 203.55],
+ '203.y': [213.35, 212.55],
+ '204.y': [159.35, 158.55],
+ '205.y': [168.35, 167.55],
+ '206.y': [177.35, 176.55],
+ '207.y': [186.35, 185.55],
+ '208.y': [195.35, 194.55],
+ '209.y': [204.35, 203.55],
+ '21.y': [213.35, 212.55],
+ '210.y': [213.35, 212.55],
+ '211.y': [159.35, 158.55],
+ '212.y': [168.35, 167.55],
+ '213.y': [177.35, 176.55],
+ '214.y': [186.35, 185.55],
+ '215.y': [195.35, 194.55],
+ '216.y': [204.35, 203.55],
+ '217.y': [213.35, 212.55],
+ '218.y': [159.35, 158.55],
+ '219.y': [168.35, 167.55],
+ '22.y': [159.35, 158.55],
+ '220.y': [177.35, 176.55],
+ '221.y': [186.35, 185.55],
+ '222.y': [195.35, 194.55],
+ '223.y': [204.35, 203.55],
+ '224.y': [213.35, 212.55],
+ '225.y': [159.35, 158.55],
+ '226.y': [168.35, 167.55],
+ '227.y': [177.35, 176.55],
+ '228.y': [186.35, 185.55],
+ '229.y': [195.35, 194.55],
+ '23.y': [168.35, 167.55],
+ '230.y': [204.35, 203.55],
+ '231.y': [213.35, 212.55],
+ '232.y': [159.35, 158.55],
+ '233.y': [168.35, 167.55],
+ '234.y': [177.35, 176.55],
+ '235.y': [186.35, 185.55],
+ '236.y': [195.35, 194.55],
+ '237.y': [204.35, 203.55],
+ '238.y': [213.35, 212.55],
+ '239.y': [159.35, 158.55],
+ '24.y': [177.35, 176.55],
+ '240.y': [168.35, 167.55],
+ '241.y': [177.35, 176.55],
+ '242.y': [186.35, 185.55],
+ '243.y': [195.35, 194.55],
+ '244.y': [204.35, 203.55],
+ '245.y': [213.35, 212.55],
+ '246.y': [159.35, 158.55],
+ '247.y': [168.35, 167.55],
+ '248.y': [177.35, 176.55],
+ '249.y': [186.35, 185.55],
+ '25.y': [186.35, 185.55],
+ '250.y': [195.35, 194.55],
+ '251.y': [204.35, 203.55],
+ '252.y': [213.35, 212.55],
+ '253.y': [159.35, 158.55],
+ '254.y': [168.35, 167.55],
+ '255.y': [177.35, 176.55],
+ '256.y': [186.35, 185.55],
+ '257.y': [195.35, 194.55],
+ '258.y': [204.35, 203.55],
+ '259.y': [213.35, 212.55],
+ '26.y': [195.35, 194.55],
+ '260.y': [159.35, 158.55],
+ '261.y': [168.35, 167.55],
+ '262.y': [177.35, 176.55],
+ '263.y': [186.35, 185.55],
+ '264.y': [195.35, 194.55],
+ '265.y': [204.35, 203.55],
+ '266.y': [213.35, 212.55],
+ '267.y': [159.35, 158.55],
+ '268.y': [168.35, 167.55],
+ '269.y': [177.35, 176.55],
+ '27.y': [204.35, 203.55],
+ '270.y': [186.35, 185.55],
+ '271.y': [195.35, 194.55],
+ '272.y': [204.35, 203.55],
+ '273.y': [213.35, 212.55],
+ '274.y': [159.35, 158.55],
+ '275.y': [168.35, 167.55],
+ '276.y': [177.35, 176.55],
+ '277.y': [186.35, 185.55],
+ '278.y': [195.35, 194.55],
+ '279.y': [204.35, 203.55],
+ '28.y': [213.35, 212.55],
+ '280.y': [213.35, 212.55],
+ '281.y': [159.35, 158.55],
+ '282.y': [168.35, 167.55],
+ '283.y': [177.35, 176.55],
+ '284.y': [186.35, 185.55],
+ '285.y': [195.35, 194.55],
+ '286.y': [204.35, 203.55],
+ '287.y': [213.35, 212.55],
+ '288.y': [159.35, 158.55],
+ '289.y': [168.35, 167.55],
+ '29.y': [159.35, 158.55],
+ '290.y': [177.35, 176.55],
+ '291.y': [186.35, 185.55],
+ '292.y': [195.35, 194.55],
+ '293.y': [204.35, 203.55],
+ '294.y': [213.35, 212.55],
+ '295.y': [159.35, 158.55],
+ '296.y': [168.35, 167.55],
+ '297.y': [177.35, 176.55],
+ '298.y': [186.35, 185.55],
+ '299.y': [195.35, 194.55],
+ '3.y': [177.35, 176.55],
+ '30.y': [168.35, 167.55],
+ '300.y': [204.35, 203.55],
+ '301.y': [213.35, 212.55],
+ '302.y': [159.35, 158.55],
+ '303.y': [168.35, 167.55],
+ '304.y': [177.35, 176.55],
+ '305.y': [186.35, 185.55],
+ '306.y': [195.35, 194.55],
+ '307.y': [204.35, 203.55],
+ '308.y': [213.35, 212.55],
+ '309.y': [159.35, 158.55],
+ '31.y': [177.35, 176.55],
+ '310.y': [168.35, 167.55],
+ '311.y': [177.35, 176.55],
+ '312.y': [186.35, 185.55],
+ '313.y': [195.35, 194.55],
+ '314.y': [204.35, 203.55],
+ '315.y': [213.35, 212.55],
+ '316.y': [159.35, 158.55],
+ '317.y': [168.35, 167.55],
+ '318.y': [177.35, 176.55],
+ '319.y': [186.35, 185.55],
+ '32.y': [186.35, 185.55],
+ '320.y': [195.35, 194.55],
+ '321.y': [204.35, 203.55],
+ '322.y': [213.35, 212.55],
+ '323.y': [159.35, 158.55],
+ '324.y': [168.35, 167.55],
+ '325.y': [177.35, 176.55],
+ '326.y': [186.35, 185.55],
+ '327.y': [195.35, 194.55],
+ '328.y': [204.35, 203.55],
+ '329.y': [213.35, 212.55],
+ '33.y': [195.35, 194.55],
+ '330.y': [159.35, 158.55],
+ '331.y': [168.35, 167.55],
+ '332.y': [177.35, 176.55],
+ '333.y': [186.35, 185.55],
+ '334.y': [195.35, 194.55],
+ '335.y': [204.35, 203.55],
+ '336.y': [213.35, 212.55],
+ '337.y': [159.35, 158.55],
+ '338.y': [168.35, 167.55],
+ '339.y': [177.35, 176.55],
+ '34.y': [204.35, 203.55],
+ '340.y': [186.35, 185.55],
+ '341.y': [195.35, 194.55],
+ '342.y': [204.35, 203.55],
+ '343.y': [213.35, 212.55],
+ '344.y': [159.35, 158.55],
+ '345.y': [168.35, 167.55],
+ '346.y': [177.35, 176.55],
+ '347.y': [186.35, 185.55],
+ '348.y': [195.35, 194.55],
+ '349.y': [204.35, 203.55],
+ '35.y': [213.35, 212.55],
+ '350.y': [213.35, 212.55],
+ '351.y': [159.35, 158.55],
+ '352.y': [168.35, 167.55],
+ '353.y': [177.35, 176.55],
+ '354.y': [186.35, 185.55],
+ '355.y': [195.35, 194.55],
+ '356.y': [204.35, 203.55],
+ '357.y': [213.35, 212.55],
+ '358.y': [159.35, 158.55],
+ '359.y': [168.35, 167.55],
+ '36.y': [159.35, 158.55],
+ '360.y': [177.35, 176.55],
+ '361.y': [186.35, 185.55],
+ '362.y': [195.35, 194.55],
+ '363.y': [204.35, 203.55],
+ '364.y': [213.35, 212.55],
+ '365.y': [255.35, 254.55],
+ '366.y': [264.35, 263.55],
+ '367.y': [273.35, 272.55],
+ '368.y': [282.35, 281.55],
+ '369.y': [291.35, 290.55],
+ '37.y': [168.35, 167.55],
+ '370.y': [300.35, 299.55],
+ '371.y': [309.35, 308.55],
+ '372.y': [255.35, 254.55],
+ '373.y': [264.35, 263.55],
+ '374.y': [273.35, 272.55],
+ '375.y': [282.35, 281.55],
+ '376.y': [291.35, 290.55],
+ '377.y': [300.35, 299.55],
+ '378.y': [309.35, 308.55],
+ '379.y': [255.35, 254.55],
+ '38.y': [177.35, 176.55],
+ '380.y': [264.35, 263.55],
+ '381.y': [273.35, 272.55],
+ '382.y': [282.35, 281.55],
+ '383.y': [291.35, 290.55],
+ '384.y': [300.35, 299.55],
+ '385.y': [309.35, 308.55],
+ '386.y': [255.35, 254.55],
+ '387.y': [264.35, 263.55],
+ '388.y': [273.35, 272.55],
+ '389.y': [282.35, 281.55],
+ '39.y': [186.35, 185.55],
+ '390.y': [291.35, 290.55],
+ '391.y': [300.35, 299.55],
+ '392.y': [309.35, 308.55],
+ '393.y': [255.35, 254.55],
+ '394.y': [264.35, 263.55],
+ '395.y': [273.35, 272.55],
+ '396.y': [282.35, 281.55],
+ '397.y': [291.35, 290.55],
+ '398.y': [300.35, 299.55],
+ '399.y': [309.35, 308.55],
+ '4.y': [186.35, 185.55],
+ '40.y': [195.35, 194.55],
+ '400.y': [255.35, 254.55],
+ '401.y': [264.35, 263.55],
+ '402.y': [273.35, 272.55],
+ '403.y': [282.35, 281.55],
+ '404.y': [291.35, 290.55],
+ '405.y': [300.35, 299.55],
+ '406.y': [309.35, 308.55],
+ '407.y': [255.35, 254.55],
+ '408.y': [264.35, 263.55],
+ '409.y': [273.35, 272.55],
+ '41.y': [204.35, 203.55],
+ '410.y': [282.35, 281.55],
+ '411.y': [291.35, 290.55],
+ '412.y': [300.35, 299.55],
+ '413.y': [309.35, 308.55],
+ '414.y': [255.35, 254.55],
+ '415.y': [264.35, 263.55],
+ '416.y': [273.35, 272.55],
+ '417.y': [282.35, 281.55],
+ '418.y': [291.35, 290.55],
+ '419.y': [300.35, 299.55],
+ '42.y': [213.35, 212.55],
+ '420.y': [309.35, 308.55],
+ '421.y': [255.35, 254.55],
+ '422.y': [264.35, 263.55],
+ '423.y': [273.35, 272.55],
+ '424.y': [282.35, 281.55],
+ '425.y': [291.35, 290.55],
+ '426.y': [300.35, 299.55],
+ '427.y': [309.35, 308.55],
+ '428.y': [255.35, 254.55],
+ '429.y': [264.35, 263.55],
+ '43.y': [159.35, 158.55],
+ '430.y': [273.35, 272.55],
+ '431.y': [282.35, 281.55],
+ '432.y': [291.35, 290.55],
+ '433.y': [300.35, 299.55],
+ '434.y': [309.35, 308.55],
+ '435.y': [255.35, 254.55],
+ '436.y': [264.35, 263.55],
+ '437.y': [273.35, 272.55],
+ '438.y': [282.35, 281.55],
+ '439.y': [291.35, 290.55],
+ '44.y': [168.35, 167.55],
+ '440.y': [300.35, 299.55],
+ '441.y': [309.35, 308.55],
+ '442.y': [255.35, 254.55],
+ '443.y': [264.35, 263.55],
+ '444.y': [273.35, 272.55],
+ '445.y': [282.35, 281.55],
+ '446.y': [291.35, 290.55],
+ '447.y': [300.35, 299.55],
+ '448.y': [309.35, 308.55],
+ '449.y': [255.35, 254.55],
+ '45.y': [177.35, 176.55],
+ '450.y': [264.35, 263.55],
+ '451.y': [273.35, 272.55],
+ '452.y': [282.35, 281.55],
+ '453.y': [291.35, 290.55],
+ '454.y': [300.35, 299.55],
+ '455.y': [309.35, 308.55],
+ '456.y': [255.35, 254.55],
+ '457.y': [264.35, 263.55],
+ '458.y': [273.35, 272.55],
+ '459.y': [282.35, 281.55],
+ '46.y': [186.35, 185.55],
+ '460.y': [291.35, 290.55],
+ '461.y': [300.35, 299.55],
+ '462.y': [309.35, 308.55],
+ '463.y': [255.35, 254.55],
+ '464.y': [264.35, 263.55],
+ '465.y': [273.35, 272.55],
+ '466.y': [282.35, 281.55],
+ '467.y': [291.35, 290.55],
+ '468.y': [300.35, 299.55],
+ '469.y': [309.35, 308.55],
+ '47.y': [195.35, 194.55],
+ '470.y': [255.35, 254.55],
+ '471.y': [264.35, 263.55],
+ '472.y': [273.35, 272.55],
+ '473.y': [282.35, 281.55],
+ '474.y': [291.35, 290.55],
+ '475.y': [300.35, 299.55],
+ '476.y': [309.35, 308.55],
+ '477.y': [255.35, 254.55],
+ '478.y': [264.35, 263.55],
+ '479.y': [273.35, 272.55],
+ '48.y': [204.35, 203.55],
+ '480.y': [282.35, 281.55],
+ '481.y': [291.35, 290.55],
+ '482.y': [300.35, 299.55],
+ '483.y': [309.35, 308.55],
+ '484.y': [255.35, 254.55],
+ '485.y': [264.35, 263.55],
+ '486.y': [273.35, 272.55],
+ '487.y': [282.35, 281.55],
+ '488.y': [291.35, 290.55],
+ '489.y': [300.35, 299.55],
+ '49.y': [213.35, 212.55],
+ '490.y': [309.35, 308.55],
+ '491.y': [255.35, 254.55],
+ '492.y': [264.35, 263.55],
+ '493.y': [273.35, 272.55],
+ '494.y': [282.35, 281.55],
+ '495.y': [291.35, 290.55],
+ '496.y': [300.35, 299.55],
+ '497.y': [309.35, 308.55],
+ '498.y': [255.35, 254.55],
+ '499.y': [264.35, 263.55],
+ '5.y': [195.35, 194.55],
+ '50.y': [159.35, 158.55],
+ '500.y': [273.35, 272.55],
+ '501.y': [282.35, 281.55],
+ '502.y': [291.35, 290.55],
+ '503.y': [300.35, 299.55],
+ '504.y': [309.35, 308.55],
+ '505.y': [255.35, 254.55],
+ '506.y': [264.35, 263.55],
+ '507.y': [273.35, 272.55],
+ '508.y': [282.35, 281.55],
+ '509.y': [291.35, 290.55],
+ '51.y': [168.35, 167.55],
+ '510.y': [300.35, 299.55],
+ '511.y': [309.35, 308.55],
+ '512.y': [255.35, 254.55],
+ '513.y': [264.35, 263.55],
+ '514.y': [273.35, 272.55],
+ '515.y': [282.35, 281.55],
+ '516.y': [291.35, 290.55],
+ '517.y': [300.35, 299.55],
+ '518.y': [309.35, 308.55],
+ '519.y': [255.35, 254.55],
+ '52.y': [177.35, 176.55],
+ '520.y': [264.35, 263.55],
+ '521.y': [273.35, 272.55],
+ '522.y': [282.35, 281.55],
+ '523.y': [291.35, 290.55],
+ '524.y': [300.35, 299.55],
+ '525.y': [309.35, 308.55],
+ '526.y': [255.35, 254.55],
+ '527.y': [264.35, 263.55],
+ '528.y': [273.35, 272.55],
+ '529.y': [282.35, 281.55],
+ '53.y': [186.35, 185.55],
+ '530.y': [291.35, 290.55],
+ '531.y': [300.35, 299.55],
+ '532.y': [309.35, 308.55],
+ '533.y': [255.35, 254.55],
+ '534.y': [264.35, 263.55],
+ '535.y': [273.35, 272.55],
+ '536.y': [282.35, 281.55],
+ '537.y': [291.35, 290.55],
+ '538.y': [300.35, 299.55],
+ '539.y': [309.35, 308.55],
+ '54.y': [195.35, 194.55],
+ '540.y': [255.35, 254.55],
+ '541.y': [264.35, 263.55],
+ '542.y': [273.35, 272.55],
+ '543.y': [282.35, 281.55],
+ '544.y': [291.35, 290.55],
+ '545.y': [300.35, 299.55],
+ '546.y': [309.35, 308.55],
+ '547.y': [255.35, 254.55],
+ '548.y': [264.35, 263.55],
+ '549.y': [273.35, 272.55],
+ '55.y': [204.35, 203.55],
+ '550.y': [282.35, 281.55],
+ '551.y': [291.35, 290.55],
+ '552.y': [300.35, 299.55],
+ '553.y': [309.35, 308.55],
+ '554.y': [255.35, 254.55],
+ '555.y': [264.35, 263.55],
+ '556.y': [273.35, 272.55],
+ '557.y': [282.35, 281.55],
+ '558.y': [291.35, 290.55],
+ '559.y': [300.35, 299.55],
+ '56.y': [213.35, 212.55],
+ '560.y': [309.35, 308.55],
+ '561.y': [255.35, 254.55],
+ '562.y': [264.35, 263.55],
+ '563.y': [273.35, 272.55],
+ '564.y': [282.35, 281.55],
+ '565.y': [291.35, 290.55],
+ '566.y': [300.35, 299.55],
+ '567.y': [309.35, 308.55],
+ '568.y': [255.35, 254.55],
+ '569.y': [264.35, 263.55],
+ '57.y': [159.35, 158.55],
+ '570.y': [273.35, 272.55],
+ '571.y': [282.35, 281.55],
+ '572.y': [291.35, 290.55],
+ '573.y': [300.35, 299.55],
+ '574.y': [309.35, 308.55],
+ '575.y': [255.35, 254.55],
+ '576.y': [264.35, 263.55],
+ '577.y': [273.35, 272.55],
+ '578.y': [282.35, 281.55],
+ '579.y': [291.35, 290.55],
+ '58.y': [168.35, 167.55],
+ '580.y': [300.35, 299.55],
+ '581.y': [309.35, 308.55],
+ '582.y': [255.35, 254.55],
+ '583.y': [264.35, 263.55],
+ '584.y': [273.35, 272.55],
+ '585.y': [282.35, 281.55],
+ '586.y': [291.35, 290.55],
+ '587.y': [300.35, 299.55],
+ '588.y': [309.35, 308.55],
+ '589.y': [255.35, 254.55],
+ '59.y': [177.35, 176.55],
+ '590.y': [264.35, 263.55],
+ '591.y': [273.35, 272.55],
+ '592.y': [282.35, 281.55],
+ '593.y': [291.35, 290.55],
+ '594.y': [300.35, 299.55],
+ '595.y': [309.35, 308.55],
+ '596.y': [255.35, 254.55],
+ '597.y': [264.35, 263.55],
+ '598.y': [273.35, 272.55],
+ '599.y': [282.35, 281.55],
+ '6.y': [204.35, 203.55],
+ '60.y': [186.35, 185.55],
+ '600.y': [291.35, 290.55],
+ '601.y': [300.35, 299.55],
+ '602.y': [309.35, 308.55],
+ '603.y': [255.35, 254.55],
+ '604.y': [264.35, 263.55],
+ '605.y': [273.35, 272.55],
+ '606.y': [282.35, 281.55],
+ '607.y': [291.35, 290.55],
+ '608.y': [300.35, 299.55],
+ '609.y': [309.35, 308.55],
+ '61.y': [195.35, 194.55],
+ '610.y': [255.35, 254.55],
+ '611.y': [264.35, 263.55],
+ '612.y': [273.35, 272.55],
+ '613.y': [282.35, 281.55],
+ '614.y': [291.35, 290.55],
+ '615.y': [300.35, 299.55],
+ '616.y': [309.35, 308.55],
+ '617.y': [255.35, 254.55],
+ '618.y': [264.35, 263.55],
+ '619.y': [273.35, 272.55],
+ '62.y': [204.35, 203.55],
+ '620.y': [282.35, 281.55],
+ '621.y': [291.35, 290.55],
+ '622.y': [300.35, 299.55],
+ '623.y': [309.35, 308.55],
+ '624.y': [255.35, 254.55],
+ '625.y': [264.35, 263.55],
+ '626.y': [273.35, 272.55],
+ '627.y': [282.35, 281.55],
+ '628.y': [291.35, 290.55],
+ '629.y': [300.35, 299.55],
+ '63.y': [213.35, 212.55],
+ '630.y': [309.35, 308.55],
+ '631.y': [255.35, 254.55],
+ '632.y': [264.35, 263.55],
+ '633.y': [273.35, 272.55],
+ '634.y': [282.35, 281.55],
+ '635.y': [291.35, 290.55],
+ '636.y': [300.35, 299.55],
+ '637.y': [309.35, 308.55],
+ '638.y': [255.35, 254.55],
+ '639.y': [264.35, 263.55],
+ '64.y': [159.35, 158.55],
+ '640.y': [273.35, 272.55],
+ '641.y': [282.35, 281.55],
+ '642.y': [291.35, 290.55],
+ '643.y': [300.35, 299.55],
+ '644.y': [309.35, 308.55],
+ '645.y': [255.35, 254.55],
+ '646.y': [264.35, 263.55],
+ '647.y': [273.35, 272.55],
+ '648.y': [282.35, 281.55],
+ '649.y': [291.35, 290.55],
+ '65.y': [168.35, 167.55],
+ '650.y': [300.35, 299.55],
+ '651.y': [309.35, 308.55],
+ '652.y': [255.35, 254.55],
+ '653.y': [264.35, 263.55],
+ '654.y': [273.35, 272.55],
+ '655.y': [282.35, 281.55],
+ '656.y': [291.35, 290.55],
+ '657.y': [300.35, 299.55],
+ '658.y': [309.35, 308.55],
+ '659.y': [255.35, 254.55],
+ '66.y': [177.35, 176.55],
+ '660.y': [264.35, 263.55],
+ '661.y': [273.35, 272.55],
+ '662.y': [282.35, 281.55],
+ '663.y': [291.35, 290.55],
+ '664.y': [300.35, 299.55],
+ '665.y': [309.35, 308.55],
+ '666.y': [255.35, 254.55],
+ '667.y': [264.35, 263.55],
+ '668.y': [273.35, 272.55],
+ '669.y': [282.35, 281.55],
+ '67.y': [186.35, 185.55],
+ '670.y': [291.35, 290.55],
+ '671.y': [300.35, 299.55],
+ '672.y': [309.35, 308.55],
+ '673.y': [255.35, 254.55],
+ '674.y': [264.35, 263.55],
+ '675.y': [273.35, 272.55],
+ '676.y': [282.35, 281.55],
+ '677.y': [291.35, 290.55],
+ '678.y': [300.35, 299.55],
+ '679.y': [309.35, 308.55],
+ '68.y': [195.35, 194.55],
+ '680.y': [255.35, 254.55],
+ '681.y': [264.35, 263.55],
+ '682.y': [273.35, 272.55],
+ '683.y': [282.35, 281.55],
+ '684.y': [291.35, 290.55],
+ '685.y': [300.35, 299.55],
+ '686.y': [309.35, 308.55],
+ '687.y': [255.35, 254.55],
+ '688.y': [264.35, 263.55],
+ '689.y': [273.35, 272.55],
+ '69.y': [204.35, 203.55],
+ '690.y': [282.35, 281.55],
+ '691.y': [291.35, 290.55],
+ '692.y': [300.35, 299.55],
+ '693.y': [309.35, 308.55],
+ '694.y': [255.35, 254.55],
+ '695.y': [264.35, 263.55],
+ '696.y': [273.35, 272.55],
+ '697.y': [282.35, 281.55],
+ '698.y': [291.35, 290.55],
+ '699.y': [300.35, 299.55],
+ '7.y': [213.35, 212.55],
+ '70.y': [213.35, 212.55],
+ '700.y': [309.35, 308.55],
+ '701.y': [255.35, 254.55],
+ '702.y': [264.35, 263.55],
+ '703.y': [273.35, 272.55],
+ '704.y': [282.35, 281.55],
+ '705.y': [291.35, 290.55],
+ '706.y': [300.35, 299.55],
+ '707.y': [309.35, 308.55],
+ '708.y': [255.35, 254.55],
+ '709.y': [264.35, 263.55],
+ '71.y': [159.35, 158.55],
+ '710.y': [273.35, 272.55],
+ '711.y': [282.35, 281.55],
+ '712.y': [291.35, 290.55],
+ '713.y': [300.35, 299.55],
+ '714.y': [309.35, 308.55],
+ '715.y': [255.35, 254.55],
+ '716.y': [264.35, 263.55],
+ '717.y': [273.35, 272.55],
+ '718.y': [282.35, 281.55],
+ '719.y': [291.35, 290.55],
+ '72.y': [168.35, 167.55],
+ '720.y': [300.35, 299.55],
+ '721.y': [309.35, 308.55],
+ '722.y': [255.35, 254.55],
+ '723.y': [264.35, 263.55],
+ '724.y': [273.35, 272.55],
+ '725.y': [282.35, 281.55],
+ '726.y': [291.35, 290.55],
+ '727.y': [300.35, 299.55],
+ '728.y': [309.35, 308.55],
+ '729.y': [255.35, 254.55],
+ '73.y': [177.35, 176.55],
+ '730.y': [264.35, 263.55],
+ '731.y': [153.35, 152.55],
+ '732.y': [153.35, 152.55],
+ '733.y': [153.35, 152.55],
+ '734.y': [153.35, 152.55],
+ '735.y': [153.35, 152.55],
+ '736.y': [153.35, 152.55],
+ '737.y': [153.35, 152.55],
+ '738.y': [153.35, 152.55],
+ '739.y': [153.35, 152.55],
+ '74.y': [186.35, 185.55],
+ '740.y': [153.35, 152.55],
+ '741.y': [153.35, 152.55],
+ '742.y': [153.35, 152.55],
+ '743.y': [153.35, 152.55],
+ '744.y': [166.7, 165.9],
+ '745.y': [184.7, 183.9],
+ '746.y': [202.7, 201.9],
+ '747.y': [249.35, 248.55],
+ '748.y': [249.35, 248.55],
+ '749.y': [249.35, 248.55],
+ '75.y': [195.35, 194.55],
+ '750.y': [249.35, 248.55],
+ '751.y': [249.35, 248.55],
+ '752.y': [249.35, 248.55],
+ '753.y': [249.35, 248.55],
+ '754.y': [249.35, 248.55],
+ '755.y': [249.35, 248.55],
+ '756.y': [249.35, 248.55],
+ '757.y': [249.35, 248.55],
+ '758.y': [249.35, 248.55],
+ '759.y': [249.35, 248.55],
+ '76.y': [204.35, 203.55],
+ '760.y': [262.7, 261.9],
+ '761.y': [280.7, 279.9],
+ '762.y': [298.7, 297.9],
+ '77.y': [213.35, 212.55],
+ '78.y': [159.35, 158.55],
+ '79.y': [168.35, 167.55],
+ '8.y': [159.35, 158.55],
+ '80.y': [177.35, 176.55],
+ '81.y': [186.35, 185.55],
+ '82.y': [195.35, 194.55],
+ '83.y': [204.35, 203.55],
+ '84.y': [213.35, 212.55],
+ '85.y': [159.35, 158.55],
+ '86.y': [168.35, 167.55],
+ '87.y': [177.35, 176.55],
+ '88.y': [186.35, 185.55],
+ '89.y': [195.35, 194.55],
+ '9.y': [168.35, 167.55],
+ '90.y': [204.35, 203.55],
+ '91.y': [213.35, 212.55],
+ '92.y': [159.35, 158.55],
+ '93.y': [168.35, 167.55],
+ '94.y': [177.35, 176.55],
+ '95.y': [186.35, 185.55],
+ '96.y': [195.35, 194.55],
+ '97.y': [204.35, 203.55],
+ '98.y': [213.35, 212.55],
+ '99.y': [159.35, 158.55],
+ },
+ legend: {
+ 'legends.0.bar.y': [331, 329.4],
+ 'legends.0.bounds.y': [331, 329.4],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-calendar.stories.tsx.calendarTwoYearSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ area: {
+ 'rect.height': [232.4, 232.2],
+ },
+ chrome: {
+ bottomAnchorY: [319.6, 319.4],
+ bottomHeight: [63.4, 63.6],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ marks: {
+ '0.y': [184.4, 184.3],
+ '1.y': [112.4, 112.3],
+ '10.y': [136.4, 136.3],
+ '100.y': [124.4, 124.3],
+ '101.y': [136.4, 136.3],
+ '102.y': [148.4, 148.3],
+ '103.y': [160.4, 160.3],
+ '104.y': [172.4, 172.3],
+ '105.y': [184.4, 184.3],
+ '106.y': [112.4, 112.3],
+ '107.y': [124.4, 124.3],
+ '108.y': [136.4, 136.3],
+ '109.y': [148.4, 148.3],
+ '11.y': [148.4, 148.3],
+ '110.y': [160.4, 160.3],
+ '111.y': [172.4, 172.3],
+ '112.y': [184.4, 184.3],
+ '113.y': [112.4, 112.3],
+ '114.y': [124.4, 124.3],
+ '115.y': [136.4, 136.3],
+ '116.y': [148.4, 148.3],
+ '117.y': [160.4, 160.3],
+ '118.y': [172.4, 172.3],
+ '119.y': [184.4, 184.3],
+ '12.y': [160.4, 160.3],
+ '120.y': [112.4, 112.3],
+ '121.y': [124.4, 124.3],
+ '122.y': [136.4, 136.3],
+ '123.y': [148.4, 148.3],
+ '124.y': [160.4, 160.3],
+ '125.y': [172.4, 172.3],
+ '126.y': [184.4, 184.3],
+ '127.y': [112.4, 112.3],
+ '128.y': [124.4, 124.3],
+ '129.y': [136.4, 136.3],
+ '13.y': [172.4, 172.3],
+ '130.y': [148.4, 148.3],
+ '131.y': [160.4, 160.3],
+ '132.y': [172.4, 172.3],
+ '133.y': [184.4, 184.3],
+ '134.y': [112.4, 112.3],
+ '135.y': [124.4, 124.3],
+ '136.y': [136.4, 136.3],
+ '137.y': [148.4, 148.3],
+ '138.y': [160.4, 160.3],
+ '139.y': [172.4, 172.3],
+ '14.y': [184.4, 184.3],
+ '140.y': [184.4, 184.3],
+ '141.y': [112.4, 112.3],
+ '142.y': [124.4, 124.3],
+ '143.y': [136.4, 136.3],
+ '144.y': [148.4, 148.3],
+ '145.y': [160.4, 160.3],
+ '146.y': [172.4, 172.3],
+ '147.y': [184.4, 184.3],
+ '148.y': [112.4, 112.3],
+ '149.y': [124.4, 124.3],
+ '15.y': [112.4, 112.3],
+ '150.y': [136.4, 136.3],
+ '151.y': [148.4, 148.3],
+ '152.y': [160.4, 160.3],
+ '153.y': [172.4, 172.3],
+ '154.y': [184.4, 184.3],
+ '155.y': [112.4, 112.3],
+ '156.y': [124.4, 124.3],
+ '157.y': [136.4, 136.3],
+ '158.y': [148.4, 148.3],
+ '159.y': [160.4, 160.3],
+ '16.y': [124.4, 124.3],
+ '160.y': [172.4, 172.3],
+ '161.y': [184.4, 184.3],
+ '162.y': [112.4, 112.3],
+ '163.y': [124.4, 124.3],
+ '164.y': [136.4, 136.3],
+ '165.y': [148.4, 148.3],
+ '166.y': [160.4, 160.3],
+ '167.y': [172.4, 172.3],
+ '168.y': [184.4, 184.3],
+ '169.y': [112.4, 112.3],
+ '17.y': [136.4, 136.3],
+ '170.y': [124.4, 124.3],
+ '171.y': [136.4, 136.3],
+ '172.y': [148.4, 148.3],
+ '173.y': [160.4, 160.3],
+ '174.y': [172.4, 172.3],
+ '175.y': [184.4, 184.3],
+ '176.y': [112.4, 112.3],
+ '177.y': [124.4, 124.3],
+ '178.y': [136.4, 136.3],
+ '179.y': [148.4, 148.3],
+ '18.y': [148.4, 148.3],
+ '180.y': [160.4, 160.3],
+ '181.y': [172.4, 172.3],
+ '182.y': [184.4, 184.3],
+ '183.y': [112.4, 112.3],
+ '184.y': [124.4, 124.3],
+ '185.y': [136.4, 136.3],
+ '186.y': [148.4, 148.3],
+ '187.y': [160.4, 160.3],
+ '188.y': [172.4, 172.3],
+ '189.y': [184.4, 184.3],
+ '19.y': [160.4, 160.3],
+ '190.y': [112.4, 112.3],
+ '191.y': [124.4, 124.3],
+ '192.y': [136.4, 136.3],
+ '193.y': [148.4, 148.3],
+ '194.y': [160.4, 160.3],
+ '195.y': [172.4, 172.3],
+ '196.y': [184.4, 184.3],
+ '197.y': [112.4, 112.3],
+ '198.y': [124.4, 124.3],
+ '199.y': [136.4, 136.3],
+ '2.y': [124.4, 124.3],
+ '20.y': [172.4, 172.3],
+ '200.y': [148.4, 148.3],
+ '201.y': [160.4, 160.3],
+ '202.y': [172.4, 172.3],
+ '203.y': [184.4, 184.3],
+ '204.y': [112.4, 112.3],
+ '205.y': [124.4, 124.3],
+ '206.y': [136.4, 136.3],
+ '207.y': [148.4, 148.3],
+ '208.y': [160.4, 160.3],
+ '209.y': [172.4, 172.3],
+ '21.y': [184.4, 184.3],
+ '210.y': [184.4, 184.3],
+ '211.y': [112.4, 112.3],
+ '212.y': [124.4, 124.3],
+ '213.y': [136.4, 136.3],
+ '214.y': [148.4, 148.3],
+ '215.y': [160.4, 160.3],
+ '216.y': [172.4, 172.3],
+ '217.y': [184.4, 184.3],
+ '218.y': [112.4, 112.3],
+ '219.y': [124.4, 124.3],
+ '22.y': [112.4, 112.3],
+ '220.y': [136.4, 136.3],
+ '221.y': [148.4, 148.3],
+ '222.y': [160.4, 160.3],
+ '223.y': [172.4, 172.3],
+ '224.y': [184.4, 184.3],
+ '225.y': [112.4, 112.3],
+ '226.y': [124.4, 124.3],
+ '227.y': [136.4, 136.3],
+ '228.y': [148.4, 148.3],
+ '229.y': [160.4, 160.3],
+ '23.y': [124.4, 124.3],
+ '230.y': [172.4, 172.3],
+ '231.y': [184.4, 184.3],
+ '232.y': [112.4, 112.3],
+ '233.y': [124.4, 124.3],
+ '234.y': [136.4, 136.3],
+ '235.y': [148.4, 148.3],
+ '236.y': [160.4, 160.3],
+ '237.y': [172.4, 172.3],
+ '238.y': [184.4, 184.3],
+ '239.y': [112.4, 112.3],
+ '24.y': [136.4, 136.3],
+ '240.y': [124.4, 124.3],
+ '241.y': [136.4, 136.3],
+ '242.y': [148.4, 148.3],
+ '243.y': [160.4, 160.3],
+ '244.y': [172.4, 172.3],
+ '245.y': [184.4, 184.3],
+ '246.y': [112.4, 112.3],
+ '247.y': [124.4, 124.3],
+ '248.y': [136.4, 136.3],
+ '249.y': [148.4, 148.3],
+ '25.y': [148.4, 148.3],
+ '250.y': [160.4, 160.3],
+ '251.y': [172.4, 172.3],
+ '252.y': [184.4, 184.3],
+ '253.y': [112.4, 112.3],
+ '254.y': [124.4, 124.3],
+ '255.y': [136.4, 136.3],
+ '256.y': [148.4, 148.3],
+ '257.y': [160.4, 160.3],
+ '258.y': [172.4, 172.3],
+ '259.y': [184.4, 184.3],
+ '26.y': [160.4, 160.3],
+ '260.y': [112.4, 112.3],
+ '261.y': [124.4, 124.3],
+ '262.y': [136.4, 136.3],
+ '263.y': [148.4, 148.3],
+ '264.y': [160.4, 160.3],
+ '265.y': [172.4, 172.3],
+ '266.y': [184.4, 184.3],
+ '267.y': [112.4, 112.3],
+ '268.y': [124.4, 124.3],
+ '269.y': [136.4, 136.3],
+ '27.y': [172.4, 172.3],
+ '270.y': [148.4, 148.3],
+ '271.y': [160.4, 160.3],
+ '272.y': [172.4, 172.3],
+ '273.y': [184.4, 184.3],
+ '274.y': [112.4, 112.3],
+ '275.y': [124.4, 124.3],
+ '276.y': [136.4, 136.3],
+ '277.y': [148.4, 148.3],
+ '278.y': [160.4, 160.3],
+ '279.y': [172.4, 172.3],
+ '28.y': [184.4, 184.3],
+ '280.y': [184.4, 184.3],
+ '281.y': [112.4, 112.3],
+ '282.y': [124.4, 124.3],
+ '283.y': [136.4, 136.3],
+ '284.y': [148.4, 148.3],
+ '285.y': [160.4, 160.3],
+ '286.y': [172.4, 172.3],
+ '287.y': [184.4, 184.3],
+ '288.y': [112.4, 112.3],
+ '289.y': [124.4, 124.3],
+ '29.y': [112.4, 112.3],
+ '290.y': [136.4, 136.3],
+ '291.y': [148.4, 148.3],
+ '292.y': [160.4, 160.3],
+ '293.y': [172.4, 172.3],
+ '294.y': [184.4, 184.3],
+ '295.y': [112.4, 112.3],
+ '296.y': [124.4, 124.3],
+ '297.y': [136.4, 136.3],
+ '298.y': [148.4, 148.3],
+ '299.y': [160.4, 160.3],
+ '3.y': [136.4, 136.3],
+ '30.y': [124.4, 124.3],
+ '300.y': [172.4, 172.3],
+ '301.y': [184.4, 184.3],
+ '302.y': [112.4, 112.3],
+ '303.y': [124.4, 124.3],
+ '304.y': [136.4, 136.3],
+ '305.y': [148.4, 148.3],
+ '306.y': [160.4, 160.3],
+ '307.y': [172.4, 172.3],
+ '308.y': [184.4, 184.3],
+ '309.y': [112.4, 112.3],
+ '31.y': [136.4, 136.3],
+ '310.y': [124.4, 124.3],
+ '311.y': [136.4, 136.3],
+ '312.y': [148.4, 148.3],
+ '313.y': [160.4, 160.3],
+ '314.y': [172.4, 172.3],
+ '315.y': [184.4, 184.3],
+ '316.y': [112.4, 112.3],
+ '317.y': [124.4, 124.3],
+ '318.y': [136.4, 136.3],
+ '319.y': [148.4, 148.3],
+ '32.y': [148.4, 148.3],
+ '320.y': [160.4, 160.3],
+ '321.y': [172.4, 172.3],
+ '322.y': [184.4, 184.3],
+ '323.y': [112.4, 112.3],
+ '324.y': [124.4, 124.3],
+ '325.y': [136.4, 136.3],
+ '326.y': [148.4, 148.3],
+ '327.y': [160.4, 160.3],
+ '328.y': [172.4, 172.3],
+ '329.y': [184.4, 184.3],
+ '33.y': [160.4, 160.3],
+ '330.y': [112.4, 112.3],
+ '331.y': [124.4, 124.3],
+ '332.y': [136.4, 136.3],
+ '333.y': [148.4, 148.3],
+ '334.y': [160.4, 160.3],
+ '335.y': [172.4, 172.3],
+ '336.y': [184.4, 184.3],
+ '337.y': [112.4, 112.3],
+ '338.y': [124.4, 124.3],
+ '339.y': [136.4, 136.3],
+ '34.y': [172.4, 172.3],
+ '340.y': [148.4, 148.3],
+ '341.y': [160.4, 160.3],
+ '342.y': [172.4, 172.3],
+ '343.y': [184.4, 184.3],
+ '344.y': [112.4, 112.3],
+ '345.y': [124.4, 124.3],
+ '346.y': [136.4, 136.3],
+ '347.y': [148.4, 148.3],
+ '348.y': [160.4, 160.3],
+ '349.y': [172.4, 172.3],
+ '35.y': [184.4, 184.3],
+ '350.y': [184.4, 184.3],
+ '351.y': [112.4, 112.3],
+ '352.y': [124.4, 124.3],
+ '353.y': [136.4, 136.3],
+ '354.y': [148.4, 148.3],
+ '355.y': [160.4, 160.3],
+ '356.y': [172.4, 172.3],
+ '357.y': [184.4, 184.3],
+ '358.y': [112.4, 112.3],
+ '359.y': [124.4, 124.3],
+ '36.y': [112.4, 112.3],
+ '360.y': [136.4, 136.3],
+ '361.y': [148.4, 148.3],
+ '362.y': [160.4, 160.3],
+ '363.y': [172.4, 172.3],
+ '364.y': [184.4, 184.3],
+ '365.y': [229.4, 229.3],
+ '366.y': [241.4, 241.3],
+ '367.y': [253.4, 253.3],
+ '368.y': [265.4, 265.3],
+ '369.y': [277.4, 277.3],
+ '37.y': [124.4, 124.3],
+ '370.y': [289.4, 289.3],
+ '371.y': [301.4, 301.3],
+ '372.y': [229.4, 229.3],
+ '373.y': [241.4, 241.3],
+ '374.y': [253.4, 253.3],
+ '375.y': [265.4, 265.3],
+ '376.y': [277.4, 277.3],
+ '377.y': [289.4, 289.3],
+ '378.y': [301.4, 301.3],
+ '379.y': [229.4, 229.3],
+ '38.y': [136.4, 136.3],
+ '380.y': [241.4, 241.3],
+ '381.y': [253.4, 253.3],
+ '382.y': [265.4, 265.3],
+ '383.y': [277.4, 277.3],
+ '384.y': [289.4, 289.3],
+ '385.y': [301.4, 301.3],
+ '386.y': [229.4, 229.3],
+ '387.y': [241.4, 241.3],
+ '388.y': [253.4, 253.3],
+ '389.y': [265.4, 265.3],
+ '39.y': [148.4, 148.3],
+ '390.y': [277.4, 277.3],
+ '391.y': [289.4, 289.3],
+ '392.y': [301.4, 301.3],
+ '393.y': [229.4, 229.3],
+ '394.y': [241.4, 241.3],
+ '395.y': [253.4, 253.3],
+ '396.y': [265.4, 265.3],
+ '397.y': [277.4, 277.3],
+ '398.y': [289.4, 289.3],
+ '399.y': [301.4, 301.3],
+ '4.y': [148.4, 148.3],
+ '40.y': [160.4, 160.3],
+ '400.y': [229.4, 229.3],
+ '401.y': [241.4, 241.3],
+ '402.y': [253.4, 253.3],
+ '403.y': [265.4, 265.3],
+ '404.y': [277.4, 277.3],
+ '405.y': [289.4, 289.3],
+ '406.y': [301.4, 301.3],
+ '407.y': [229.4, 229.3],
+ '408.y': [241.4, 241.3],
+ '409.y': [253.4, 253.3],
+ '41.y': [172.4, 172.3],
+ '410.y': [265.4, 265.3],
+ '411.y': [277.4, 277.3],
+ '412.y': [289.4, 289.3],
+ '413.y': [301.4, 301.3],
+ '414.y': [229.4, 229.3],
+ '415.y': [241.4, 241.3],
+ '416.y': [253.4, 253.3],
+ '417.y': [265.4, 265.3],
+ '418.y': [277.4, 277.3],
+ '419.y': [289.4, 289.3],
+ '42.y': [184.4, 184.3],
+ '420.y': [301.4, 301.3],
+ '421.y': [229.4, 229.3],
+ '422.y': [241.4, 241.3],
+ '423.y': [253.4, 253.3],
+ '424.y': [265.4, 265.3],
+ '425.y': [277.4, 277.3],
+ '426.y': [289.4, 289.3],
+ '427.y': [301.4, 301.3],
+ '428.y': [229.4, 229.3],
+ '429.y': [241.4, 241.3],
+ '43.y': [112.4, 112.3],
+ '430.y': [253.4, 253.3],
+ '431.y': [265.4, 265.3],
+ '432.y': [277.4, 277.3],
+ '433.y': [289.4, 289.3],
+ '434.y': [301.4, 301.3],
+ '435.y': [229.4, 229.3],
+ '436.y': [241.4, 241.3],
+ '437.y': [253.4, 253.3],
+ '438.y': [265.4, 265.3],
+ '439.y': [277.4, 277.3],
+ '44.y': [124.4, 124.3],
+ '440.y': [289.4, 289.3],
+ '441.y': [301.4, 301.3],
+ '442.y': [229.4, 229.3],
+ '443.y': [241.4, 241.3],
+ '444.y': [253.4, 253.3],
+ '445.y': [265.4, 265.3],
+ '446.y': [277.4, 277.3],
+ '447.y': [289.4, 289.3],
+ '448.y': [301.4, 301.3],
+ '449.y': [229.4, 229.3],
+ '45.y': [136.4, 136.3],
+ '450.y': [241.4, 241.3],
+ '451.y': [253.4, 253.3],
+ '452.y': [265.4, 265.3],
+ '453.y': [277.4, 277.3],
+ '454.y': [289.4, 289.3],
+ '455.y': [301.4, 301.3],
+ '456.y': [229.4, 229.3],
+ '457.y': [241.4, 241.3],
+ '458.y': [253.4, 253.3],
+ '459.y': [265.4, 265.3],
+ '46.y': [148.4, 148.3],
+ '460.y': [277.4, 277.3],
+ '461.y': [289.4, 289.3],
+ '462.y': [301.4, 301.3],
+ '463.y': [229.4, 229.3],
+ '464.y': [241.4, 241.3],
+ '465.y': [253.4, 253.3],
+ '466.y': [265.4, 265.3],
+ '467.y': [277.4, 277.3],
+ '468.y': [289.4, 289.3],
+ '469.y': [301.4, 301.3],
+ '47.y': [160.4, 160.3],
+ '470.y': [229.4, 229.3],
+ '471.y': [241.4, 241.3],
+ '472.y': [253.4, 253.3],
+ '473.y': [265.4, 265.3],
+ '474.y': [277.4, 277.3],
+ '475.y': [289.4, 289.3],
+ '476.y': [301.4, 301.3],
+ '477.y': [229.4, 229.3],
+ '478.y': [241.4, 241.3],
+ '479.y': [253.4, 253.3],
+ '48.y': [172.4, 172.3],
+ '480.y': [265.4, 265.3],
+ '481.y': [277.4, 277.3],
+ '482.y': [289.4, 289.3],
+ '483.y': [301.4, 301.3],
+ '484.y': [229.4, 229.3],
+ '485.y': [241.4, 241.3],
+ '486.y': [253.4, 253.3],
+ '487.y': [265.4, 265.3],
+ '488.y': [277.4, 277.3],
+ '489.y': [289.4, 289.3],
+ '49.y': [184.4, 184.3],
+ '490.y': [301.4, 301.3],
+ '491.y': [229.4, 229.3],
+ '492.y': [241.4, 241.3],
+ '493.y': [253.4, 253.3],
+ '494.y': [265.4, 265.3],
+ '495.y': [277.4, 277.3],
+ '496.y': [289.4, 289.3],
+ '497.y': [301.4, 301.3],
+ '498.y': [229.4, 229.3],
+ '499.y': [241.4, 241.3],
+ '5.y': [160.4, 160.3],
+ '50.y': [112.4, 112.3],
+ '500.y': [253.4, 253.3],
+ '501.y': [265.4, 265.3],
+ '502.y': [277.4, 277.3],
+ '503.y': [289.4, 289.3],
+ '504.y': [301.4, 301.3],
+ '505.y': [229.4, 229.3],
+ '506.y': [241.4, 241.3],
+ '507.y': [253.4, 253.3],
+ '508.y': [265.4, 265.3],
+ '509.y': [277.4, 277.3],
+ '51.y': [124.4, 124.3],
+ '510.y': [289.4, 289.3],
+ '511.y': [301.4, 301.3],
+ '512.y': [229.4, 229.3],
+ '513.y': [241.4, 241.3],
+ '514.y': [253.4, 253.3],
+ '515.y': [265.4, 265.3],
+ '516.y': [277.4, 277.3],
+ '517.y': [289.4, 289.3],
+ '518.y': [301.4, 301.3],
+ '519.y': [229.4, 229.3],
+ '52.y': [136.4, 136.3],
+ '520.y': [241.4, 241.3],
+ '521.y': [253.4, 253.3],
+ '522.y': [265.4, 265.3],
+ '523.y': [277.4, 277.3],
+ '524.y': [289.4, 289.3],
+ '525.y': [301.4, 301.3],
+ '526.y': [229.4, 229.3],
+ '527.y': [241.4, 241.3],
+ '528.y': [253.4, 253.3],
+ '529.y': [265.4, 265.3],
+ '53.y': [148.4, 148.3],
+ '530.y': [277.4, 277.3],
+ '531.y': [289.4, 289.3],
+ '532.y': [301.4, 301.3],
+ '533.y': [229.4, 229.3],
+ '534.y': [241.4, 241.3],
+ '535.y': [253.4, 253.3],
+ '536.y': [265.4, 265.3],
+ '537.y': [277.4, 277.3],
+ '538.y': [289.4, 289.3],
+ '539.y': [301.4, 301.3],
+ '54.y': [160.4, 160.3],
+ '540.y': [229.4, 229.3],
+ '541.y': [241.4, 241.3],
+ '542.y': [253.4, 253.3],
+ '543.y': [265.4, 265.3],
+ '544.y': [277.4, 277.3],
+ '545.y': [289.4, 289.3],
+ '546.y': [301.4, 301.3],
+ '547.y': [229.4, 229.3],
+ '548.y': [241.4, 241.3],
+ '549.y': [253.4, 253.3],
+ '55.y': [172.4, 172.3],
+ '550.y': [265.4, 265.3],
+ '551.y': [277.4, 277.3],
+ '552.y': [289.4, 289.3],
+ '553.y': [301.4, 301.3],
+ '554.y': [229.4, 229.3],
+ '555.y': [241.4, 241.3],
+ '556.y': [253.4, 253.3],
+ '557.y': [265.4, 265.3],
+ '558.y': [277.4, 277.3],
+ '559.y': [289.4, 289.3],
+ '56.y': [184.4, 184.3],
+ '560.y': [301.4, 301.3],
+ '561.y': [229.4, 229.3],
+ '562.y': [241.4, 241.3],
+ '563.y': [253.4, 253.3],
+ '564.y': [265.4, 265.3],
+ '565.y': [277.4, 277.3],
+ '566.y': [289.4, 289.3],
+ '567.y': [301.4, 301.3],
+ '568.y': [229.4, 229.3],
+ '569.y': [241.4, 241.3],
+ '57.y': [112.4, 112.3],
+ '570.y': [253.4, 253.3],
+ '571.y': [265.4, 265.3],
+ '572.y': [277.4, 277.3],
+ '573.y': [289.4, 289.3],
+ '574.y': [301.4, 301.3],
+ '575.y': [229.4, 229.3],
+ '576.y': [241.4, 241.3],
+ '577.y': [253.4, 253.3],
+ '578.y': [265.4, 265.3],
+ '579.y': [277.4, 277.3],
+ '58.y': [124.4, 124.3],
+ '580.y': [289.4, 289.3],
+ '581.y': [301.4, 301.3],
+ '582.y': [229.4, 229.3],
+ '583.y': [241.4, 241.3],
+ '584.y': [253.4, 253.3],
+ '585.y': [265.4, 265.3],
+ '586.y': [277.4, 277.3],
+ '587.y': [289.4, 289.3],
+ '588.y': [301.4, 301.3],
+ '589.y': [229.4, 229.3],
+ '59.y': [136.4, 136.3],
+ '590.y': [241.4, 241.3],
+ '591.y': [253.4, 253.3],
+ '592.y': [265.4, 265.3],
+ '593.y': [277.4, 277.3],
+ '594.y': [289.4, 289.3],
+ '595.y': [301.4, 301.3],
+ '596.y': [229.4, 229.3],
+ '597.y': [241.4, 241.3],
+ '598.y': [253.4, 253.3],
+ '599.y': [265.4, 265.3],
+ '6.y': [172.4, 172.3],
+ '60.y': [148.4, 148.3],
+ '600.y': [277.4, 277.3],
+ '601.y': [289.4, 289.3],
+ '602.y': [301.4, 301.3],
+ '603.y': [229.4, 229.3],
+ '604.y': [241.4, 241.3],
+ '605.y': [253.4, 253.3],
+ '606.y': [265.4, 265.3],
+ '607.y': [277.4, 277.3],
+ '608.y': [289.4, 289.3],
+ '609.y': [301.4, 301.3],
+ '61.y': [160.4, 160.3],
+ '610.y': [229.4, 229.3],
+ '611.y': [241.4, 241.3],
+ '612.y': [253.4, 253.3],
+ '613.y': [265.4, 265.3],
+ '614.y': [277.4, 277.3],
+ '615.y': [289.4, 289.3],
+ '616.y': [301.4, 301.3],
+ '617.y': [229.4, 229.3],
+ '618.y': [241.4, 241.3],
+ '619.y': [253.4, 253.3],
+ '62.y': [172.4, 172.3],
+ '620.y': [265.4, 265.3],
+ '621.y': [277.4, 277.3],
+ '622.y': [289.4, 289.3],
+ '623.y': [301.4, 301.3],
+ '624.y': [229.4, 229.3],
+ '625.y': [241.4, 241.3],
+ '626.y': [253.4, 253.3],
+ '627.y': [265.4, 265.3],
+ '628.y': [277.4, 277.3],
+ '629.y': [289.4, 289.3],
+ '63.y': [184.4, 184.3],
+ '630.y': [301.4, 301.3],
+ '631.y': [229.4, 229.3],
+ '632.y': [241.4, 241.3],
+ '633.y': [253.4, 253.3],
+ '634.y': [265.4, 265.3],
+ '635.y': [277.4, 277.3],
+ '636.y': [289.4, 289.3],
+ '637.y': [301.4, 301.3],
+ '638.y': [229.4, 229.3],
+ '639.y': [241.4, 241.3],
+ '64.y': [112.4, 112.3],
+ '640.y': [253.4, 253.3],
+ '641.y': [265.4, 265.3],
+ '642.y': [277.4, 277.3],
+ '643.y': [289.4, 289.3],
+ '644.y': [301.4, 301.3],
+ '645.y': [229.4, 229.3],
+ '646.y': [241.4, 241.3],
+ '647.y': [253.4, 253.3],
+ '648.y': [265.4, 265.3],
+ '649.y': [277.4, 277.3],
+ '65.y': [124.4, 124.3],
+ '650.y': [289.4, 289.3],
+ '651.y': [301.4, 301.3],
+ '652.y': [229.4, 229.3],
+ '653.y': [241.4, 241.3],
+ '654.y': [253.4, 253.3],
+ '655.y': [265.4, 265.3],
+ '656.y': [277.4, 277.3],
+ '657.y': [289.4, 289.3],
+ '658.y': [301.4, 301.3],
+ '659.y': [229.4, 229.3],
+ '66.y': [136.4, 136.3],
+ '660.y': [241.4, 241.3],
+ '661.y': [253.4, 253.3],
+ '662.y': [265.4, 265.3],
+ '663.y': [277.4, 277.3],
+ '664.y': [289.4, 289.3],
+ '665.y': [301.4, 301.3],
+ '666.y': [229.4, 229.3],
+ '667.y': [241.4, 241.3],
+ '668.y': [253.4, 253.3],
+ '669.y': [265.4, 265.3],
+ '67.y': [148.4, 148.3],
+ '670.y': [277.4, 277.3],
+ '671.y': [289.4, 289.3],
+ '672.y': [301.4, 301.3],
+ '673.y': [229.4, 229.3],
+ '674.y': [241.4, 241.3],
+ '675.y': [253.4, 253.3],
+ '676.y': [265.4, 265.3],
+ '677.y': [277.4, 277.3],
+ '678.y': [289.4, 289.3],
+ '679.y': [301.4, 301.3],
+ '68.y': [160.4, 160.3],
+ '680.y': [229.4, 229.3],
+ '681.y': [241.4, 241.3],
+ '682.y': [253.4, 253.3],
+ '683.y': [265.4, 265.3],
+ '684.y': [277.4, 277.3],
+ '685.y': [289.4, 289.3],
+ '686.y': [301.4, 301.3],
+ '687.y': [229.4, 229.3],
+ '688.y': [241.4, 241.3],
+ '689.y': [253.4, 253.3],
+ '69.y': [172.4, 172.3],
+ '690.y': [265.4, 265.3],
+ '691.y': [277.4, 277.3],
+ '692.y': [289.4, 289.3],
+ '693.y': [301.4, 301.3],
+ '694.y': [229.4, 229.3],
+ '695.y': [241.4, 241.3],
+ '696.y': [253.4, 253.3],
+ '697.y': [265.4, 265.3],
+ '698.y': [277.4, 277.3],
+ '699.y': [289.4, 289.3],
+ '7.y': [184.4, 184.3],
+ '70.y': [184.4, 184.3],
+ '700.y': [301.4, 301.3],
+ '701.y': [229.4, 229.3],
+ '702.y': [241.4, 241.3],
+ '703.y': [253.4, 253.3],
+ '704.y': [265.4, 265.3],
+ '705.y': [277.4, 277.3],
+ '706.y': [289.4, 289.3],
+ '707.y': [301.4, 301.3],
+ '708.y': [229.4, 229.3],
+ '709.y': [241.4, 241.3],
+ '71.y': [112.4, 112.3],
+ '710.y': [253.4, 253.3],
+ '711.y': [265.4, 265.3],
+ '712.y': [277.4, 277.3],
+ '713.y': [289.4, 289.3],
+ '714.y': [301.4, 301.3],
+ '715.y': [229.4, 229.3],
+ '716.y': [241.4, 241.3],
+ '717.y': [253.4, 253.3],
+ '718.y': [265.4, 265.3],
+ '719.y': [277.4, 277.3],
+ '72.y': [124.4, 124.3],
+ '720.y': [289.4, 289.3],
+ '721.y': [301.4, 301.3],
+ '722.y': [229.4, 229.3],
+ '723.y': [241.4, 241.3],
+ '724.y': [253.4, 253.3],
+ '725.y': [265.4, 265.3],
+ '726.y': [277.4, 277.3],
+ '727.y': [289.4, 289.3],
+ '728.y': [301.4, 301.3],
+ '729.y': [229.4, 229.3],
+ '73.y': [136.4, 136.3],
+ '730.y': [241.4, 241.3],
+ '731.y': [106.4, 106.3],
+ '732.y': [106.4, 106.3],
+ '733.y': [106.4, 106.3],
+ '734.y': [106.4, 106.3],
+ '735.y': [106.4, 106.3],
+ '736.y': [106.4, 106.3],
+ '737.y': [106.4, 106.3],
+ '738.y': [106.4, 106.3],
+ '739.y': [106.4, 106.3],
+ '74.y': [148.4, 148.3],
+ '740.y': [106.4, 106.3],
+ '741.y': [106.4, 106.3],
+ '742.y': [106.4, 106.3],
+ '743.y': [106.4, 106.3],
+ '744.y': [121.25, 121.15],
+ '745.y': [145.25, 145.15],
+ '746.y': [169.25, 169.15],
+ '747.y': [223.4, 223.3],
+ '748.y': [223.4, 223.3],
+ '749.y': [223.4, 223.3],
+ '75.y': [160.4, 160.3],
+ '750.y': [223.4, 223.3],
+ '751.y': [223.4, 223.3],
+ '752.y': [223.4, 223.3],
+ '753.y': [223.4, 223.3],
+ '754.y': [223.4, 223.3],
+ '755.y': [223.4, 223.3],
+ '756.y': [223.4, 223.3],
+ '757.y': [223.4, 223.3],
+ '758.y': [223.4, 223.3],
+ '759.y': [223.4, 223.3],
+ '76.y': [172.4, 172.3],
+ '760.y': [238.25, 238.15],
+ '761.y': [262.25, 262.15],
+ '762.y': [286.25, 286.15],
+ '77.y': [184.4, 184.3],
+ '78.y': [112.4, 112.3],
+ '79.y': [124.4, 124.3],
+ '8.y': [112.4, 112.3],
+ '80.y': [136.4, 136.3],
+ '81.y': [148.4, 148.3],
+ '82.y': [160.4, 160.3],
+ '83.y': [172.4, 172.3],
+ '84.y': [184.4, 184.3],
+ '85.y': [112.4, 112.3],
+ '86.y': [124.4, 124.3],
+ '87.y': [136.4, 136.3],
+ '88.y': [148.4, 148.3],
+ '89.y': [160.4, 160.3],
+ '9.y': [124.4, 124.3],
+ '90.y': [172.4, 172.3],
+ '91.y': [184.4, 184.3],
+ '92.y': [112.4, 112.3],
+ '93.y': [124.4, 124.3],
+ '94.y': [136.4, 136.3],
+ '95.y': [148.4, 148.3],
+ '96.y': [160.4, 160.3],
+ '97.y': [172.4, 172.3],
+ '98.y': [184.4, 184.3],
+ '99.y': [112.4, 112.3],
+ },
+ legend: {
+ 'legends.0.bar.y': [327.6, 327.4],
+ 'legends.0.bounds.y': [327.6, 327.4],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-calendar.stories.tsx.calendarCompactSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-calendar.stories.tsx.calendarCompactSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-color-legend.stories.tsx.hotDaysSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-color-legend.stories.tsx.hotDaysSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-color-legend.stories.tsx.aqiSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-color-legend.stories.tsx.aqiSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-color-legend.stories.tsx.tradeBalanceSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-color-legend.stories.tsx.tradeBalanceSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-distribution.stories.tsx.histogramSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-distribution.stories.tsx.histogramSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-distribution.stories.tsx.densitySpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-distribution.stories.tsx.densitySpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-donuts.stories.tsx.donutSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ area: {
+ 'rect.height': [221.7, 234.1],
+ },
+ chrome: {
+ bottomAnchorY: [332, 344.4],
+ bottomHeight: [52, 39.6],
+ 'source.maxWidth': [223, 292.58],
+ },
+ marks: {
+ '0.cy': [221.15, 227.35],
+ '0.innerRadius': [56.53, 59.7],
+ '0.outerRadius': [94.22, 99.49],
+ '1.cy': [221.15, 227.35],
+ '1.innerRadius': [56.53, 59.7],
+ '1.outerRadius': [94.22, 99.49],
+ '2.cy': [221.15, 227.35],
+ '2.innerRadius': [56.53, 59.7],
+ '2.outerRadius': [94.22, 99.49],
+ '3.cy': [221.15, 227.35],
+ '3.innerRadius': [56.53, 59.7],
+ '3.outerRadius': [94.22, 99.49],
+ '4.cy': [221.15, 227.35],
+ '4.innerRadius': [56.53, 59.7],
+ '4.outerRadius': [94.22, 99.49],
+ '5.cy': [221.15, 227.35],
+ '5.innerRadius': [56.53, 59.7],
+ '5.outerRadius': [94.22, 99.49],
+ '6.cy': [221.15, 227.35],
+ '6.innerRadius': [56.53, 59.7],
+ '6.outerRadius': [94.22, 99.49],
+ },
+ labels: {
+ '0.label.x': [265, 268.48],
+ '0.label.y': [134.66, 136.89],
+ '1.label.x': [286.04, 290.56],
+ '1.label.y': [269.27, 278.18],
+ '2.label.x': [106.65, 105.89],
+ '2.label.y': [319.65, 331.07],
+ '3.label.x': [41.09, 36.43],
+ '3.label.y': [263.93, 272.58],
+ '4.label.x': [23.26, 18.14],
+ '4.label.y': [189.19, 194.14],
+ '5.label.x': [71.95, 68.26],
+ '5.label.y': [138.75, 141.18],
+ '6.label.x': [116.6, 115.28],
+ '6.label.y': [111.72, 112.82],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-donuts.stories.tsx.donutSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-dual-axis.stories.tsx.dualAxisSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-dual-axis.stories.tsx.dualAxisSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-dual-axis.stories.tsx.scatterTrendlineSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-dual-axis.stories.tsx.scatterTrendlineSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-lines.stories.tsx.gdpLineSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ area: {
+ 'rect.height': [132.25, 144.65],
+ },
+ chrome: {
+ bottomAnchorY: [332, 344.4],
+ bottomHeight: [52, 39.6],
+ 'source.maxWidth': [223, 292.58],
+ },
+ axes: {
+ 'x.end.y': [306, 318.4],
+ 'x.start.y': [306, 318.4],
+ 'y.end.y': [306, 318.4],
+ 'y.tick.0.position': [289.47, 300.32],
+ 'y.tick.1.position': [248.14, 255.12],
+ 'y.tick.2.position': [206.81, 209.91],
+ 'y.titlePosition.y': [239.88, 246.08],
+ },
+ marks: {
+ '0.height': [76.87, 84.08],
+ '0.y': [199.37, 201.78],
+ '1.height': [96.71, 105.78],
+ '1.y': [204.33, 207.2],
+ '2.height': [123.16, 134.71],
+ '2.y': [181.19, 181.89],
+ },
+ annotations: {
+ 'a0.rect.height': [132.25, 144.65],
+ 'a1.line.end.y': [248.14, 255.12],
+ 'a1.line.start.y': [248.14, 255.12],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-lines.stories.tsx.gdpLineSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-lines.stories.tsx.compactGdpSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ area: {
+ 'rect.height': [185.15, 197.55],
+ },
+ chrome: {
+ bottomAnchorY: [332, 344.4],
+ bottomHeight: [52, 39.6],
+ 'source.maxWidth': [223, 292.58],
+ },
+ axes: {
+ 'x.end.y': [306, 318.4],
+ 'x.start.y': [306, 318.4],
+ 'y.end.y': [306, 318.4],
+ 'y.tick.0.position': [282.86, 293.71],
+ 'y.tick.1.position': [225, 231.97],
+ 'y.tick.2.position': [167.14, 170.24],
+ 'y.titlePosition.y': [213.43, 219.63],
+ },
+ marks: {
+ '0.height': [107.62, 114.83],
+ '0.y': [156.72, 159.13],
+ '1.height': [135.39, 144.46],
+ '1.y': [163.67, 166.53],
+ '2.height': [172.42, 183.97],
+ '2.y': [131.26, 131.96],
+ },
+ annotations: {
+ 'a0.rect.height': [185.15, 197.55],
+ 'a1.line.end.y': [225, 231.97],
+ 'a1.line.start.y': [225, 231.97],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-lines.stories.tsx.compactGdpSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-lines.stories.tsx.fiveSeriesSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-lines.stories.tsx.multiSeriesAreaOverlapSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ area: {
+ 'rect.height': [87.9, 101.9],
+ },
+ chrome: {
+ bottomAnchorY: [276, 290],
+ bottomHeight: [108, 94],
+ 'byline.maxWidth': [223, 292.58],
+ 'byline.y': [78, 64],
+ 'source.maxWidth': [223, 292.58],
+ },
+ axes: {
+ 'x.end.y': [250, 264],
+ 'x.start.y': [250, 264],
+ 'y.end.y': [250, 264],
+ 'y.tick.0.position': [250, 264],
+ 'y.tick.1.position': [232.42, 243.62],
+ 'y.tick.2.position': [214.84, 223.24],
+ 'y.tick.3.position': [197.26, 202.86],
+ 'y.tick.4.position': [179.68, 182.48],
+ },
+ marks: {
+ '0.height': [73.84, 85.6],
+ '0.y': [176.16, 178.4],
+ '1.height': [45.71, 52.99],
+ '1.y': [204.29, 211.01],
+ '2.height': [40.43, 46.87],
+ '2.y': [209.57, 217.13],
+ '3.height': [52.74, 61.14],
+ '3.y': [197.26, 202.86],
+ '4.height': [31.64, 36.68],
+ '4.y': [176.16, 178.4],
+ '5.height': [7.03, 8.15],
+ '5.y': [204.29, 211.01],
+ '6.height': [1.76, 2.04],
+ '6.y': [209.57, 217.13],
+ '7.height': [42.19, 48.91],
+ '7.y': [197.26, 202.86],
+ },
+ annotations: {
+ 'a0.connector.from.x': [324.5, 323.26],
+ 'a0.connector.from.y': [181.86, 181.95],
+ 'a0.connector.to.x': [331.4, 331.72],
+ 'a0.connector.to.y': [198.87, 205.75],
+ 'a0.dot.y': [206.05, 213.05],
+ },
+ legend: {
+ 'legends.0.bounds.y': [276, 290],
+ 'legends.0.entry.0.y': [276, 290],
+ 'legends.0.entry.1.y': [276, 290],
+ 'legends.0.entry.2.y': [276, 290],
+ 'legends.0.entry.3.y': [292, 306],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-lines.stories.tsx.multiSeriesAreaOverlapSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-lines.stories.tsx.multiSeriesAreaStackedSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-lines.stories.tsx.multiSeriesAreaStackedSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-lines.stories.tsx.editorialSingleLineDarkSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'brand.maxWidth': [130, 74.2],
+ 'byline.maxWidth': [223, 271.8],
+ 'source.maxWidth': [223, 271.8],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-lines.stories.tsx.editorialSingleLineDarkSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'brand.maxWidth': [130, 81.62],
+ 'byline.maxWidth': [1111, 1152.38],
+ 'source.maxWidth': [1111, 1152.38],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-lines.stories.tsx.lineSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-lines.stories.tsx.lineSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-maps.stories.tsx.usBubblesSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [235, 304.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-maps.stories.tsx.usBubblesSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-maps.stories.tsx.worldDivergingSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [235, 304.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-maps.stories.tsx.worldDivergingSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-maps.stories.tsx.usStatesCategoricalSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [235, 304.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-maps.stories.tsx.usStatesCategoricalSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-misc.stories.tsx.energyFlowSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [215, 284.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-misc.stories.tsx.energyFlowSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-misc.stories.tsx.chromeSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-misc.stories.tsx.chromeSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-misc.stories.tsx.fullChromeSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [223, 292.58],
+ 'footer.maxWidth': [223, 292.58],
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-misc.stories.tsx.fullChromeSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'footer.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-misc.stories.tsx.rotatedSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-misc.stories.tsx.rotatedSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-parliament.stories.tsx.usHouseSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-parliament.stories.tsx.usHouseSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-parliament.stories.tsx.euSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-parliament.stories.tsx.euSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-parliament.stories.tsx.electionDonutSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-parliament.stories.tsx.electionDonutSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-parliament.stories.tsx.resultsBarSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-parliament.stories.tsx.resultsBarSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-ranges.stories.tsx.rangeDumbbellSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-ranges.stories.tsx.rangeDumbbellSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-ranges.stories.tsx.rangeArrowSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ area: {
+ 'rect.height': [156.3, 168.7],
+ },
+ chrome: {
+ bottomAnchorY: [332, 344.4],
+ bottomHeight: [52, 39.6],
+ 'source.maxWidth': [223, 292.58],
+ },
+ axes: {
+ 'x.end.y': [284, 296.4],
+ 'x.start.y': [284, 296.4],
+ 'x.titlePosition.y': [319, 331.4],
+ 'y.end.y': [284, 296.4],
+ 'y.tick.0.position': [268.37, 279.53],
+ 'y.tick.1.position': [243.36, 252.54],
+ 'y.tick.2.position': [218.35, 225.55],
+ 'y.tick.3.position': [193.35, 198.55],
+ 'y.tick.4.position': [168.34, 171.56],
+ 'y.tick.5.position': [143.33, 144.57],
+ },
+ marks: {
+ '0.y1': [193.35, 198.55],
+ '0.y2': [193.35, 198.55],
+ '1.y1': [189.35, 194.55],
+ '1.y2': [193.35, 198.55],
+ '10.y1': [222.35, 229.55],
+ '10.y2': [218.35, 225.55],
+ '11.y1': [214.35, 221.55],
+ '11.y2': [218.35, 225.55],
+ '12.y1': [268.37, 279.53],
+ '12.y2': [268.37, 279.53],
+ '13.y1': [264.37, 275.53],
+ '13.y2': [268.37, 279.53],
+ '14.y1': [272.37, 283.53],
+ '14.y2': [268.37, 279.53],
+ '15.y1': [243.36, 252.54],
+ '15.y2': [243.36, 252.54],
+ '16.y1': [247.36, 256.54],
+ '16.y2': [243.36, 252.54],
+ '17.y1': [239.36, 248.54],
+ '17.y2': [243.36, 252.54],
+ '2.y1': [197.35, 202.55],
+ '2.y2': [193.35, 198.55],
+ '3.y1': [143.33, 144.57],
+ '3.y2': [143.33, 144.57],
+ '4.y1': [147.33, 148.57],
+ '4.y2': [143.33, 144.57],
+ '5.y1': [139.33, 140.57],
+ '5.y2': [143.33, 144.57],
+ '6.y1': [168.34, 171.56],
+ '6.y2': [168.34, 171.56],
+ '7.y1': [164.34, 167.56],
+ '7.y2': [168.34, 171.56],
+ '8.y1': [172.34, 175.56],
+ '8.y2': [168.34, 171.56],
+ '9.y1': [218.35, 225.55],
+ '9.y2': [218.35, 225.55],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-ranges.stories.tsx.rangeArrowSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-ranges.stories.tsx.rangeBarSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ area: {
+ 'rect.height': [163.68, 176.08],
+ },
+ chrome: {
+ bottomAnchorY: [332, 344.4],
+ bottomHeight: [52, 39.6],
+ 'source.maxWidth': [223, 292.58],
+ },
+ axes: {
+ 'x.end.y': [291.38, 303.78],
+ 'x.start.y': [291.38, 303.78],
+ 'y.end.y': [291.38, 303.78],
+ 'y.tick.0.position': [291.38, 303.78],
+ 'y.tick.1.position': [240.23, 248.76],
+ 'y.tick.2.position': [189.08, 193.73],
+ 'y.tick.3.position': [137.93, 138.71],
+ 'y.titlePosition.y': [209.54, 215.74],
+ },
+ marks: {
+ '0.height': [29.67, 31.91],
+ '0.y': [243.81, 252.61],
+ '1.height': [42.45, 45.67],
+ '1.y': [244.32, 253.16],
+ '10.height': [37.34, 40.17],
+ '10.y': [231.02, 238.85],
+ '11.height': [28.64, 30.81],
+ '11.y': [241.25, 249.86],
+ '2.height': [38.87, 41.82],
+ '2.y': [236.14, 244.35],
+ '3.height': [49.1, 52.82],
+ '3.y': [194.2, 199.23],
+ '4.height': [51.66, 55.58],
+ '4.y': [178.34, 182.18],
+ '5.height': [43.99, 47.32],
+ '5.y': [161.46, 164.02],
+ '6.height': [41.43, 44.57],
+ '6.y': [136.91, 137.6],
+ '7.height': [37.85, 40.72],
+ '7.y': [150.21, 151.91],
+ '8.height': [35.81, 38.52],
+ '8.y': [167.09, 170.07],
+ '9.height': [35.29, 37.97],
+ '9.y': [192.66, 197.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-ranges.stories.tsx.rangeBarSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-ranking.stories.tsx.slopeMarketShareSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ area: {
+ 'rect.height': [178.7, 191.1],
+ },
+ chrome: {
+ bottomAnchorY: [332, 344.4],
+ bottomHeight: [52, 39.6],
+ 'source.maxWidth': [223, 292.58],
+ },
+ axes: {
+ 'x.end.y': [306, 318.4],
+ 'x.start.y': [306, 318.4],
+ },
+ marks: {
+ '0.height': [25.81, 27.6],
+ '0.y': [131.27, 131.55],
+ '1.height': [132.04, 141.2],
+ '1.y': [170.98, 174.01],
+ '2.height': [47.65, 50.96],
+ '2.y': [160.06, 162.34],
+ '3.height': [43.68, 46.71],
+ '3.y': [210.69, 216.48],
+ '4.height': [3.97, 4.25],
+ '4.y': [259.34, 268.5],
+ },
+ endpointLabels: {
+ '0.labelY': [150.58, 152.65],
+ '1.labelY': [283.75, 296.15],
+ '2.labelY': [172.83, 174.9],
+ '3.labelY': [204.19, 209.98],
+ '4.labelY': [252.84, 262],
+ 'bounds.height': [178.7, 191.1],
+ 'leadingBounds.height': [178.7, 191.1],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-ranking.stories.tsx.slopeMarketShareSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-ranking.stories.tsx.bumpConstructorsSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-ranking.stories.tsx.bumpConstructorsSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-refresh.stories.tsx.flatColumnsSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ area: {
+ 'rect.height': [142.13, 156.13],
+ },
+ chrome: {
+ bottomAnchorY: [312, 326],
+ bottomHeight: [72, 58],
+ 'byline.maxWidth': [223, 292.58],
+ 'byline.y': [42, 28],
+ 'source.maxWidth': [223, 292.58],
+ },
+ axes: {
+ 'x.end.y': [269.83, 283.83],
+ 'x.start.y': [269.83, 283.83],
+ 'y.end.y': [269.83, 283.83],
+ 'y.tick.0.position': [269.83, 283.83],
+ 'y.tick.1.position': [225.41, 235.04],
+ 'y.tick.2.position': [181, 186.25],
+ 'y.tick.3.position': [136.58, 137.46],
+ 'y.titlePosition.y': [198.76, 205.76],
+ },
+ marks: {
+ '0.height': [126.14, 138.56],
+ '0.y': [143.69, 145.26],
+ '1.height': [128.8, 141.49],
+ '1.y': [141.02, 142.34],
+ '2.height': [122.59, 134.66],
+ '2.y': [147.24, 149.17],
+ '3.height': [132.36, 145.4],
+ '3.y': [137.47, 138.43],
+ '4.height': [135.02, 148.32],
+ '4.y': [134.81, 135.51],
+ '5.height': [119.92, 131.73],
+ '5.y': [149.91, 152.1],
+ '6.height': [114.59, 125.88],
+ '6.y': [155.24, 157.95],
+ '7.height': [118.14, 129.78],
+ '7.y': [151.68, 154.05],
+ },
+ labels: {
+ '0.label.y': [131.69, 133.26],
+ '1.label.y': [129.02, 130.34],
+ '2.label.y': [135.24, 137.17],
+ '3.label.y': [125.47, 126.43],
+ '4.label.y': [122.81, 123.51],
+ '5.label.y': [137.91, 140.1],
+ '6.label.y': [143.24, 145.95],
+ '7.label.y': [139.68, 142.05],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-refresh.stories.tsx.flatColumnsSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1111, 1180.58],
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-refresh.stories.tsx.centerLabelSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-refresh.stories.tsx.centerLabelSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-refresh.stories.tsx.presetSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-refresh.stories.tsx.presetSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-sankey.stories.tsx.baseSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [215, 284.58],
+ 'source.maxWidth': [215, 284.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-sankey.stories.tsx.baseSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1105, 1174.58],
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-sankey.stories.tsx.otherSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [215, 284.58],
+ 'source.maxWidth': [215, 284.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-sankey.stories.tsx.otherSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'byline.maxWidth': [1105, 1174.58],
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-search.stories.tsx.findYourCountrySpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-search.stories.tsx.findYourCountrySpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-tables.stories.tsx.basicSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 390],
+ },
+ chrome: {
+ bottomHeight: [56, 43.6],
+ 'source.maxWidth': [215, 284.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-tables.stories.tsx.basicSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 1280],
+ },
+ chrome: {
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-tables.stories.tsx.deltaSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 390],
+ },
+ chrome: {
+ bottomHeight: [56, 43.6],
+ 'source.maxWidth': [215, 284.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-tables.stories.tsx.deltaSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 1280],
+ },
+ chrome: {
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-tables.stories.tsx.heatmapSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 390],
+ },
+ chrome: {
+ bottomHeight: [56, 43.6],
+ 'source.maxWidth': [215, 284.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-tables.stories.tsx.heatmapSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 1280],
+ },
+ chrome: {
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-tables.stories.tsx.sparklineSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 390],
+ },
+ chrome: {
+ 'source.maxWidth': [215, 284.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-tables.stories.tsx.sparklineSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 1280],
+ },
+ chrome: {
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-tables.stories.tsx.cardsSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 390],
+ },
+ chrome: {
+ bottomHeight: [56, 43.6],
+ 'source.maxWidth': [215, 284.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-tables.stories.tsx.cardsSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 1280],
+ },
+ chrome: {
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-tables.stories.tsx.explicitWidthSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 390],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-tables.stories.tsx.explicitWidthSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.width': [undefined, 1280],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-tilemaps.stories.tsx.quantitativeSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ bottomAnchorY: [undefined, 313.3],
+ 'byline.maxWidth': [215, 284.58],
+ 'source.maxWidth': [215, 284.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-tilemaps.stories.tsx.quantitativeSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ bottomAnchorY: [undefined, 309.2],
+ 'byline.maxWidth': [1105, 1174.58],
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-tilemaps.stories.tsx.categoricalDarkSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ dims: {
+ 'size.height': [411.75, 397.75],
+ },
+ area: {
+ 'rect.height': [199.25, 213.25],
+ },
+ chrome: {
+ bottomAnchorY: [undefined, 325.75],
+ bottomHeight: [76, 62],
+ 'byline.maxWidth': [215, 284.58],
+ 'byline.y': [42, 28],
+ 'source.maxWidth': [215, 284.58],
+ },
+ marks: {
+ '0.size': [17, 19],
+ '0.x': [285.5, 293.5],
+ '1.size': [17, 19],
+ '1.x': [263.5, 269.5],
+ '1.y': [136.75, 138.75],
+ '10.size': [17, 19],
+ '10.x': [241.5, 245.5],
+ '10.y': [158.75, 162.75],
+ '11.size': [17, 19],
+ '11.x': [263.5, 269.5],
+ '11.y': [158.75, 162.75],
+ '12.size': [17, 19],
+ '12.x': [65.5, 53.5],
+ '12.y': [180.75, 186.75],
+ '13.size': [17, 19],
+ '13.x': [87.5, 77.5],
+ '13.y': [180.75, 186.75],
+ '14.size': [17, 19],
+ '14.x': [109.5, 101.5],
+ '14.y': [180.75, 186.75],
+ '15.size': [17, 19],
+ '15.x': [131.5, 125.5],
+ '15.y': [180.75, 186.75],
+ '16.size': [17, 19],
+ '16.x': [153.5, 149.5],
+ '16.y': [180.75, 186.75],
+ '17.size': [17, 19],
+ '17.x': [175.5, 173.5],
+ '17.y': [180.75, 186.75],
+ '18.size': [17, 19],
+ '18.y': [180.75, 186.75],
+ '19.size': [17, 19],
+ '19.x': [219.5, 221.5],
+ '19.y': [180.75, 186.75],
+ '2.size': [17, 19],
+ '2.x': [285.5, 293.5],
+ '2.y': [136.75, 138.75],
+ '20.size': [17, 19],
+ '20.x': [241.5, 245.5],
+ '20.y': [180.75, 186.75],
+ '21.size': [17, 19],
+ '21.x': [263.5, 269.5],
+ '21.y': [180.75, 186.75],
+ '22.size': [17, 19],
+ '22.x': [285.5, 293.5],
+ '22.y': [180.75, 186.75],
+ '23.size': [17, 19],
+ '23.x': [65.5, 53.5],
+ '23.y': [202.75, 210.75],
+ '24.size': [17, 19],
+ '24.x': [87.5, 77.5],
+ '24.y': [202.75, 210.75],
+ '25.size': [17, 19],
+ '25.x': [109.5, 101.5],
+ '25.y': [202.75, 210.75],
+ '26.size': [17, 19],
+ '26.x': [131.5, 125.5],
+ '26.y': [202.75, 210.75],
+ '27.size': [17, 19],
+ '27.x': [153.5, 149.5],
+ '27.y': [202.75, 210.75],
+ '28.size': [17, 19],
+ '28.x': [175.5, 173.5],
+ '28.y': [202.75, 210.75],
+ '29.size': [17, 19],
+ '29.y': [202.75, 210.75],
+ '3.size': [17, 19],
+ '3.x': [65.5, 53.5],
+ '3.y': [158.75, 162.75],
+ '30.size': [17, 19],
+ '30.x': [219.5, 221.5],
+ '30.y': [202.75, 210.75],
+ '31.size': [17, 19],
+ '31.x': [241.5, 245.5],
+ '31.y': [202.75, 210.75],
+ '32.size': [17, 19],
+ '32.x': [263.5, 269.5],
+ '32.y': [202.75, 210.75],
+ '33.size': [17, 19],
+ '33.x': [285.5, 293.5],
+ '33.y': [202.75, 210.75],
+ '34.size': [17, 19],
+ '34.x': [87.5, 77.5],
+ '34.y': [224.75, 234.75],
+ '35.size': [17, 19],
+ '35.x': [109.5, 101.5],
+ '35.y': [224.75, 234.75],
+ '36.size': [17, 19],
+ '36.x': [131.5, 125.5],
+ '36.y': [224.75, 234.75],
+ '37.size': [17, 19],
+ '37.x': [153.5, 149.5],
+ '37.y': [224.75, 234.75],
+ '38.size': [17, 19],
+ '38.x': [175.5, 173.5],
+ '38.y': [224.75, 234.75],
+ '39.size': [17, 19],
+ '39.y': [224.75, 234.75],
+ '4.size': [17, 19],
+ '4.x': [87.5, 77.5],
+ '4.y': [158.75, 162.75],
+ '40.size': [17, 19],
+ '40.x': [219.5, 221.5],
+ '40.y': [224.75, 234.75],
+ '41.size': [17, 19],
+ '41.x': [241.5, 245.5],
+ '41.y': [224.75, 234.75],
+ '42.size': [17, 19],
+ '42.x': [65.5, 53.5],
+ '42.y': [246.75, 258.75],
+ '43.size': [17, 19],
+ '43.x': [131.5, 125.5],
+ '43.y': [246.75, 258.75],
+ '44.size': [17, 19],
+ '44.x': [153.5, 149.5],
+ '44.y': [246.75, 258.75],
+ '45.size': [17, 19],
+ '45.x': [175.5, 173.5],
+ '45.y': [246.75, 258.75],
+ '46.size': [17, 19],
+ '46.y': [246.75, 258.75],
+ '47.size': [17, 19],
+ '47.x': [219.5, 221.5],
+ '47.y': [246.75, 258.75],
+ '48.size': [17, 19],
+ '48.x': [87.5, 77.5],
+ '48.y': [268.75, 282.75],
+ '49.size': [17, 19],
+ '49.x': [131.5, 125.5],
+ '49.y': [268.75, 282.75],
+ '5.size': [17, 19],
+ '5.x': [109.5, 101.5],
+ '5.y': [158.75, 162.75],
+ '50.size': [17, 19],
+ '50.x': [219.5, 221.5],
+ '50.y': [268.75, 282.75],
+ '6.size': [17, 19],
+ '6.x': [131.5, 125.5],
+ '6.y': [158.75, 162.75],
+ '7.size': [17, 19],
+ '7.x': [153.5, 149.5],
+ '7.y': [158.75, 162.75],
+ '8.size': [17, 19],
+ '8.x': [175.5, 173.5],
+ '8.y': [158.75, 162.75],
+ '9.size': [17, 19],
+ '9.y': [158.75, 162.75],
+ },
+ labels: {
+ '0.label.x': [294, 303],
+ '0.label.y': [123.25, 124.25],
+ '1.label.x': [272, 279],
+ '1.label.y': [145.25, 148.25],
+ '10.label.x': [250, 255],
+ '10.label.y': [167.25, 172.25],
+ '11.label.x': [272, 279],
+ '11.label.y': [167.25, 172.25],
+ '12.label.x': [74, 63],
+ '12.label.y': [189.25, 196.25],
+ '13.label.x': [96, 87],
+ '13.label.y': [189.25, 196.25],
+ '14.label.x': [118, 111],
+ '14.label.y': [189.25, 196.25],
+ '15.label.x': [140, 135],
+ '15.label.y': [189.25, 196.25],
+ '16.label.x': [162, 159],
+ '16.label.y': [189.25, 196.25],
+ '17.label.x': [184, 183],
+ '17.label.y': [189.25, 196.25],
+ '18.label.x': [206, 207],
+ '18.label.y': [189.25, 196.25],
+ '19.label.x': [228, 231],
+ '19.label.y': [189.25, 196.25],
+ '2.label.x': [294, 303],
+ '2.label.y': [145.25, 148.25],
+ '20.label.x': [250, 255],
+ '20.label.y': [189.25, 196.25],
+ '21.label.x': [272, 279],
+ '21.label.y': [189.25, 196.25],
+ '22.label.x': [294, 303],
+ '22.label.y': [189.25, 196.25],
+ '23.label.x': [74, 63],
+ '23.label.y': [211.25, 220.25],
+ '24.label.x': [96, 87],
+ '24.label.y': [211.25, 220.25],
+ '25.label.x': [118, 111],
+ '25.label.y': [211.25, 220.25],
+ '26.label.x': [140, 135],
+ '26.label.y': [211.25, 220.25],
+ '27.label.x': [162, 159],
+ '27.label.y': [211.25, 220.25],
+ '28.label.x': [184, 183],
+ '28.label.y': [211.25, 220.25],
+ '29.label.x': [206, 207],
+ '29.label.y': [211.25, 220.25],
+ '3.label.x': [74, 63],
+ '3.label.y': [167.25, 172.25],
+ '30.label.x': [228, 231],
+ '30.label.y': [211.25, 220.25],
+ '31.label.x': [250, 255],
+ '31.label.y': [211.25, 220.25],
+ '32.label.x': [272, 279],
+ '32.label.y': [211.25, 220.25],
+ '33.label.x': [294, 303],
+ '33.label.y': [211.25, 220.25],
+ '34.label.x': [96, 87],
+ '34.label.y': [233.25, 244.25],
+ '35.label.x': [118, 111],
+ '35.label.y': [233.25, 244.25],
+ '36.label.x': [140, 135],
+ '36.label.y': [233.25, 244.25],
+ '37.label.x': [162, 159],
+ '37.label.y': [233.25, 244.25],
+ '38.label.x': [184, 183],
+ '38.label.y': [233.25, 244.25],
+ '39.label.x': [206, 207],
+ '39.label.y': [233.25, 244.25],
+ '4.label.x': [96, 87],
+ '4.label.y': [167.25, 172.25],
+ '40.label.x': [228, 231],
+ '40.label.y': [233.25, 244.25],
+ '41.label.x': [250, 255],
+ '41.label.y': [233.25, 244.25],
+ '42.label.x': [74, 63],
+ '42.label.y': [255.25, 268.25],
+ '43.label.x': [140, 135],
+ '43.label.y': [255.25, 268.25],
+ '44.label.x': [162, 159],
+ '44.label.y': [255.25, 268.25],
+ '45.label.x': [184, 183],
+ '45.label.y': [255.25, 268.25],
+ '46.label.x': [206, 207],
+ '46.label.y': [255.25, 268.25],
+ '47.label.x': [228, 231],
+ '47.label.y': [255.25, 268.25],
+ '48.label.x': [96, 87],
+ '48.label.y': [277.25, 292.25],
+ '49.label.x': [140, 135],
+ '49.label.y': [277.25, 292.25],
+ '5.label.x': [118, 111],
+ '5.label.y': [167.25, 172.25],
+ '50.label.x': [228, 231],
+ '50.label.y': [277.25, 292.25],
+ '6.label.x': [140, 135],
+ '6.label.y': [167.25, 172.25],
+ '7.label.x': [162, 159],
+ '7.label.y': [167.25, 172.25],
+ '8.label.x': [184, 183],
+ '8.label.y': [167.25, 172.25],
+ '9.label.x': [206, 207],
+ '9.label.y': [167.25, 172.25],
+ },
+ legend: {
+ 'categoricalLegend.bounds.height': [32, 16],
+ 'categoricalLegend.bounds.width': [259, 283],
+ 'categoricalLegend.bounds.x': [65.5, 53.5],
+ 'categoricalLegend.bounds.y': [293.75, 309.75],
+ },
+ },
+ style: {
+ 'marks.0.label.style.fontSize': [8.5, 9.5],
+ 'marks.0.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.1.label.style.fontSize': [8.5, 9.5],
+ 'marks.1.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.10.label.style.fontSize': [8.5, 9.5],
+ 'marks.10.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.11.label.style.fontSize': [8.5, 9.5],
+ 'marks.11.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.12.label.style.fontSize': [8.5, 9.5],
+ 'marks.12.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.13.label.style.fontSize': [8.5, 9.5],
+ 'marks.13.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.14.label.style.fontSize': [8.5, 9.5],
+ 'marks.14.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.15.label.style.fontSize': [8.5, 9.5],
+ 'marks.15.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.16.label.style.fontSize': [8.5, 9.5],
+ 'marks.16.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.17.label.style.fontSize': [8.5, 9.5],
+ 'marks.17.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.18.label.style.fontSize': [8.5, 9.5],
+ 'marks.18.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.19.label.style.fontSize': [8.5, 9.5],
+ 'marks.19.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.2.label.style.fontSize': [8.5, 9.5],
+ 'marks.2.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.20.label.style.fontSize': [8.5, 9.5],
+ 'marks.20.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.21.label.style.fontSize': [8.5, 9.5],
+ 'marks.21.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.22.label.style.fontSize': [8.5, 9.5],
+ 'marks.22.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.23.label.style.fontSize': [8.5, 9.5],
+ 'marks.23.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.24.label.style.fontSize': [8.5, 9.5],
+ 'marks.24.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.25.label.style.fontSize': [8.5, 9.5],
+ 'marks.25.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.26.label.style.fontSize': [8.5, 9.5],
+ 'marks.26.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.27.label.style.fontSize': [8.5, 9.5],
+ 'marks.27.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.28.label.style.fontSize': [8.5, 9.5],
+ 'marks.28.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.29.label.style.fontSize': [8.5, 9.5],
+ 'marks.29.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.3.label.style.fontSize': [8.5, 9.5],
+ 'marks.3.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.30.label.style.fontSize': [8.5, 9.5],
+ 'marks.30.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.31.label.style.fontSize': [8.5, 9.5],
+ 'marks.31.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.32.label.style.fontSize': [8.5, 9.5],
+ 'marks.32.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.33.label.style.fontSize': [8.5, 9.5],
+ 'marks.33.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.34.label.style.fontSize': [8.5, 9.5],
+ 'marks.34.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.35.label.style.fontSize': [8.5, 9.5],
+ 'marks.35.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.36.label.style.fontSize': [8.5, 9.5],
+ 'marks.36.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.37.label.style.fontSize': [8.5, 9.5],
+ 'marks.37.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.38.label.style.fontSize': [8.5, 9.5],
+ 'marks.38.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.39.label.style.fontSize': [8.5, 9.5],
+ 'marks.39.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.4.label.style.fontSize': [8.5, 9.5],
+ 'marks.4.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.40.label.style.fontSize': [8.5, 9.5],
+ 'marks.40.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.41.label.style.fontSize': [8.5, 9.5],
+ 'marks.41.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.42.label.style.fontSize': [8.5, 9.5],
+ 'marks.42.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.43.label.style.fontSize': [8.5, 9.5],
+ 'marks.43.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.44.label.style.fontSize': [8.5, 9.5],
+ 'marks.44.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.45.label.style.fontSize': [8.5, 9.5],
+ 'marks.45.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.46.label.style.fontSize': [8.5, 9.5],
+ 'marks.46.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.47.label.style.fontSize': [8.5, 9.5],
+ 'marks.47.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.48.label.style.fontSize': [8.5, 9.5],
+ 'marks.48.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.49.label.style.fontSize': [8.5, 9.5],
+ 'marks.49.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.5.label.style.fontSize': [8.5, 9.5],
+ 'marks.5.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.50.label.style.fontSize': [8.5, 9.5],
+ 'marks.50.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.6.label.style.fontSize': [8.5, 9.5],
+ 'marks.6.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.7.label.style.fontSize': [8.5, 9.5],
+ 'marks.7.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.8.label.style.fontSize': [8.5, 9.5],
+ 'marks.8.valueLabel.style.fontSize': [7.1, 8],
+ 'marks.9.label.style.fontSize': [8.5, 9.5],
+ 'marks.9.valueLabel.style.fontSize': [7.1, 8],
+ },
+ },
+ '../testing/fixtures-tilemaps.stories.tsx.categoricalDarkSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ bottomAnchorY: [undefined, 311.2],
+ 'byline.maxWidth': [1105, 1174.58],
+ 'source.maxWidth': [1105, 1174.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-waffle.stories.tsx.waffleBasicSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-waffle.stories.tsx.waffleBasicSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-waffle.stories.tsx.waffleHighlightSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-waffle.stories.tsx.waffleHighlightSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-you-draw-it.stories.tsx.youDrawItSpec @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/fixtures-you-draw-it.stories.tsx.youDrawItSpec @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/histogram-formatting.stories.tsx.pooled @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/histogram-formatting.stories.tsx.pooled @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/histogram-formatting.stories.tsx.percentages @390': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [223, 292.58],
+ },
+ },
+ style: {},
+ },
+ '../testing/histogram-formatting.stories.tsx.percentages @1280': {
+ reason:
+ 'C7 shared measured footer replaces the fixed brand reserve; attribution is complete and automatic geometry uses the remaining frame.',
+ changes: {
+ chrome: {
+ 'source.maxWidth': [1111, 1180.58],
+ },
+ },
+ style: {},
+ },
+};
diff --git a/examples/src/__tests__/migration-v8-parity.test.ts b/examples/src/__tests__/migration-v8-parity.test.ts
new file mode 100644
index 00000000..ea9c942f
--- /dev/null
+++ b/examples/src/__tests__/migration-v8-parity.test.ts
@@ -0,0 +1,549 @@
+///
+/**
+ * v8 -> v9 migration parity, measured against real v8.9.1 compile output.
+ *
+ * Fixtures come from `bun run test:parity:capture` (scripts/capture-v8-parity.mjs):
+ * every authored v8.9.1 example spec compiled by v8.9.1 at 390px and 1280px.
+ * Here each spec goes through `migrateSpecV8()` and the v9 engine at the same size,
+ * and both layouts are reduced by the shared `parity-signature.ts` extractor.
+ *
+ * Gate (RFC section 7 acceptance: "migrated v8 specs keep v8 styling; layout is
+ * not promised pixel-identical"):
+ * - style equals v8 except the exact additive control metadata and B1 font fixes
+ * listed below (every other flattened field compares in both directions).
+ * - geometry must be identical unless a D14 layout heuristic applies. Of the D14
+ * items, only placement order, connector-crossing scoring, source-order note
+ * numbering and wrapped rich-text notes can move geometry, and all four act on
+ * text annotations (the color and refline/range size items are style, which
+ * migration materializes; token stamping is renderer-only; inferFieldType moved
+ * nothing in this corpus; the gallery `labelPlacement` rewrites only touch v9
+ * examples). So a spec without text annotations must match v8 exactly. A spec
+ * with them may move only in the categories its GEOMETRY_ALLOWLIST entry names,
+ * and the entry cites the D14 item. Entries must have text annotations and must
+ * move in every listed category, so the list cannot go stale silently.
+ *
+ * PARITY_REPORT=1 prints a summary table; PARITY_DUMP= also writes every
+ * fixture's v8/v9 signatures and deltas as JSON for investigation.
+ */
+process.env.TZ = 'UTC';
+
+import { readFileSync, writeFileSync } from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { gunzipSync } from 'node:zlib';
+import usCountiesAlbers from 'us-atlas/counties-albers-10m.json';
+import usStates from 'us-atlas/states-10m.json';
+import usStatesAlbers from 'us-atlas/states-albers-10m.json';
+import { describe, expect, it } from 'vitest';
+import worldCountries from 'world-atlas/countries-110m.json';
+import {
+ compileBarList,
+ compileChart,
+ compileGeoMap,
+ compileGraph,
+ compileLayer,
+ compileSankey,
+ compileTable,
+ compileTileMap,
+} from '../../../packages/engine/src/compile';
+import { migrateSpecV8 } from '../../../packages/engine/src/migration/v8';
+import {
+ APPROVED_AXIS_TITLE_LAYOUT,
+ APPROVED_B3_MAP_LAYOUT,
+ APPROVED_ENDPOINT_TOP_SWEEP,
+ APPROVED_MAP_KEY_PLACEMENT,
+ APPROVED_PRE_RFC_LAYOUT,
+ APPROVED_RFC_B1_LAYOUT,
+} from './migration-v8-approved-changes';
+import { APPROVED_C7_ATTRIBUTION } from './migration-v8-attribution-changes';
+import {
+ flatten,
+ GEOMETRY_CATEGORIES,
+ type Geometry,
+ type Signature,
+ signature,
+} from './parity-signature';
+
+interface Entry {
+ name: string;
+ width: number;
+ spec: unknown;
+ signature: Signature;
+}
+const capture = JSON.parse(
+ gunzipSync(
+ readFileSync(
+ path.join(path.dirname(fileURLToPath(import.meta.url)), '__fixtures__/v8-parity.json.gz'),
+ ),
+ ).toString(),
+) as { tag: string; commit: string; entries: Entry[]; skipped: unknown[] };
+
+const topologies: Record = {
+ 'us-atlas/counties-albers-10m.json': usCountiesAlbers,
+ 'us-atlas/states-10m.json': usStates,
+ 'us-atlas/states-albers-10m.json': usStatesAlbers,
+ 'world-atlas/countries-110m.json': worldCountries,
+};
+const hydrate = (value: unknown): unknown =>
+ JSON.parse(JSON.stringify(value), (_key, child) =>
+ child && typeof child === 'object' && typeof child.$topology === 'string'
+ ? topologies[child.$topology]
+ : child,
+ );
+
+// biome-ignore lint/suspicious/noExplicitAny: fixtures are untyped v8 JSON.
+function compile(spec: any, width: number) {
+ const options = { width, height: 400, onWarn: () => {} };
+ if ('layer' in spec) return compileLayer(spec, options);
+ if ('mark' in spec) return compileChart(spec, options);
+ switch (spec.type) {
+ case 'table':
+ return compileTable(spec, options);
+ case 'graph':
+ return compileGraph(spec, options);
+ case 'sankey':
+ return compileSankey(spec, options);
+ case 'map':
+ return compileGeoMap(spec, options);
+ case 'barlist':
+ return compileBarList(spec, options);
+ case 'tilemap':
+ return compileTileMap(spec, options);
+ }
+ throw new Error(`Unknown spec kind for ${JSON.stringify(Object.keys(spec))}`);
+}
+
+interface StyleDiff {
+ path: string;
+ v8: unknown;
+ v9: unknown;
+}
+function styleDiff(v8: Signature['style'], v9: Signature['style']): StyleDiff[] {
+ const before = flatten(v8);
+ const after = flatten(v9);
+ const diffs: StyleDiff[] = [];
+ for (const [path, value] of Object.entries(before))
+ if (!(path in after) || after[path] !== value) diffs.push({ path, v8: value, v9: after[path] });
+ for (const [path, value] of Object.entries(after))
+ if (!(path in before)) diffs.push({ path, v8: undefined, v9: value });
+ return diffs;
+}
+
+interface CategoryDelta {
+ /** Largest absolute px difference over numeric values present on both sides. */
+ maxAbs: number;
+ /** Values added, removed, or changed in kind or text (tick labels, note text). */
+ structural: number;
+}
+function geometryDelta(v8: Geometry, v9: Geometry) {
+ const out: Record = {};
+ for (const category of GEOMETRY_CATEGORIES) {
+ const before = v8[category] ?? {};
+ const after = v9[category] ?? {};
+ const delta = { maxAbs: 0, structural: 0 };
+ for (const path of new Set([...Object.keys(before), ...Object.keys(after)])) {
+ const a = before[path];
+ const b = after[path];
+ if (typeof a === 'number' && typeof b === 'number')
+ delta.maxAbs = Math.max(delta.maxAbs, Math.round(Math.abs(a - b) * 100) / 100);
+ else if (a !== b) delta.structural++;
+ }
+ out[category] = delta;
+ }
+ return out;
+}
+
+type Category = (typeof GEOMETRY_CATEGORIES)[number];
+/** Plot shrink from a taller reserved note band moves everything below the chrome. */
+const NOTE_BAND: Category[] = ['area', 'chrome', 'axes', 'marks', 'annotations', 'notes'];
+const NUMBERING = 'D14 source-order note numbering (RFC section 4 "Numbering (v9, D14)")';
+const WRAPPED =
+ 'D14 wrapped rich-text notes: the subtitle gets its own note line, and the reserved ' +
+ 'note band (8.11 stabilization) grows, shrinking the plot (RFC section 4 "Note content")';
+const PLAN_HEIGHT =
+ 'v9 layout plan sizes y ticks against the final plot height (v8 left the subtract-mode ' +
+ 'note band out and picked ticks for a taller plot: 8 ticks 16px apart at 130px) ' +
+ '(RFC section 7: layout is not promised pixel-identical)';
+const CURVE_TAIL =
+ 'curve connectors are shaped to the data point and shortened along their end tangent, ' +
+ 'so the tail aims at the marker (v8 shortened along the chord)';
+const PLACEMENT =
+ 'D14 placement order and connector-crossing scoring (RFC section 4 "Placement order ' +
+ 'and scoring (v9, D14)")';
+
+/**
+ * Specs whose geometry moves under v9 because of a D14 heuristic (RFC section 2
+ * D14; section 9 phase 5 "D14 retunes"), with the categories allowed to move.
+ * Keyed by fixture name; applies to both widths.
+ */
+const GEOMETRY_ALLOWLIST: Record = {
+ '../gallery/charts-scatter-distribution.stories.tsx.bubbleSpec': {
+ d14: NUMBERING,
+ categories: ['notes'],
+ },
+ '../gallery/charts-scatter-distribution.stories.tsx.colorScatterSpec': {
+ d14: NUMBERING,
+ categories: ['notes'],
+ },
+ // v9 also emits display lines for one-line notes (wrapped notes).
+ '../gallery/charts-scatter-distribution.stories.tsx.trendScatterSpec': {
+ d14: `${NUMBERING}; ${WRAPPED}`,
+ categories: ['notes'],
+ },
+ '../gallery/features-annotations.stories.tsx.textAnnotationSpec': {
+ d14: WRAPPED,
+ categories: NOTE_BAND,
+ },
+ '../gallery/features-annotations.stories.tsx.ledeSpec': { d14: WRAPPED, categories: NOTE_BAND },
+ // At 1280 the new candidate scoring also demotes "Straight, no arrow" to a note.
+ '../gallery/features-annotations.stories.tsx.connectorsSpec': {
+ d14: `${WRAPPED}; ${PLACEMENT}`,
+ categories: NOTE_BAND,
+ },
+ '../gallery/features-annotations.stories.tsx.thinningSpec': {
+ d14: `${NUMBERING}; ${PLACEMENT}`,
+ categories: ['annotations', 'notes'],
+ },
+ '../gallery/features-annotations.stories.tsx.richTextSpec': {
+ d14: CURVE_TAIL,
+ categories: ['annotations'],
+ },
+ '../gallery/showcase.stories.tsx.scatterSpec': {
+ d14: CURVE_TAIL,
+ categories: ['annotations'],
+ },
+ '../testing/fixtures-lines.stories.tsx.lineSpec': {
+ d14: PLAN_HEIGHT,
+ categories: ['axes'],
+ },
+ '../testing/fixtures-bars.stories.tsx.tempSpec': {
+ d14: PLACEMENT,
+ categories: ['annotations'],
+ },
+};
+
+const RETIRED_LABELS =
+ 'RFC 8.1 "End-of-line labels": v9 retires per-mark line-end labels and migration ' +
+ 'rewrites them to a label-only endpoint column, which has no per-label font settings ' +
+ 'and reserves its own column beside the plot';
+
+/**
+ * Specs whose STYLE differs from v8 because a documented migration rewrite has
+ * no style-preserving v9 equivalent. Keyed by fixture name; applies to both
+ * widths. `style` lists flattened style path prefixes that may differ (exact
+ * path or a `prefix.` descendant); `geometry` lists categories that may move.
+ * Every listed prefix and category must differ at some width.
+ */
+const STYLE_ALLOWLIST: Record = {
+ '../gallery/features-data-encoding.stories.tsx.dashEncodingSpec': {
+ reason: RETIRED_LABELS,
+ style: ['endpointLabels', 'marks.0.label', 'marks.1.label', 'marks.2.label'],
+ geometry: ['area', 'axes', 'marks', 'labels', 'endpointLabels'],
+ },
+};
+/**
+ * Style paths allowed to differ in every fixture, each with its reason. Endpoint
+ * markers are centered on the line's last point (where hover dots, the
+ * crosshair and annotation anchors sit) instead of offset by their radius, so
+ * their fill must be opaque to cover the line end.
+ */
+const CORPUS_STYLE_CHANGES: Array<{ pattern: RegExp; reason: string }> = [
+ {
+ pattern: /^endpointLabels\.(entries|leading)\.\d+\.marker\.fill$/,
+ reason: 'endpoint markers center on the data point with an opaque knockout fill',
+ },
+];
+// These are additive resolved metadata, not a license to change existing style.
+const ENDPOINT_CONTROL_METADATA: Record = {
+ 'theme.fonts.sizes.endpointLabel': 13,
+ 'theme.fonts.sizes.endpointValue': 12,
+ 'theme.spacing.endpointEntryGap': 6,
+ 'theme.spacing.endpointValueGap': 4,
+ 'theme.spacing.endpointColumnGap': 18,
+};
+const VALUE_FONT_PATH = /^marks(?:\.facet\[[^\]]+\])?\.\d+\.label\.style\.fontFamily$/;
+const VALUE_FONT_REASON =
+ 'B1: bar/column, dot and range value labels follow the resolved theme font instead of hardcoded system-ui; pie already did so.';
+function sanctionedValueFontChange(diff: StyleDiff, next: Signature | undefined): boolean {
+ if (
+ !next ||
+ !VALUE_FONT_PATH.test(diff.path) ||
+ diff.v8 !== 'system-ui, -apple-system, sans-serif'
+ )
+ return false;
+ const fields = flatten(next.style);
+ const mark = diff.path.slice(0, -'.label.style.fontFamily'.length);
+ return (
+ ['rect', 'point'].includes(String(fields[`${mark}.type`])) &&
+ diff.v9 === fields['theme.fonts.family']
+ );
+}
+const sanctionedEndpointPrecisionChange = (diff: StyleDiff) => {
+ if (
+ !/^endpointLabels\.(entries|leading)\.\d+\.value$/.test(diff.path) ||
+ typeof diff.v8 !== 'string' ||
+ typeof diff.v9 !== 'string'
+ )
+ return false;
+ if (!/^[+−-]?\d+(?:\.\d+)?$/.test(diff.v8) || !/^[+−-]?\d+(?:\.\d+)?$/.test(diff.v9))
+ return false;
+ const trimPrecision = (value: string) =>
+ value.includes('.') ? value.replace(/0+$/, '').replace(/\.$/, '') : value;
+ return trimPrecision(diff.v8) === trimPrecision(diff.v9);
+};
+const additiveEndpointMetadata = (diff: StyleDiff) =>
+ diff.v8 === undefined &&
+ Object.hasOwn(ENDPOINT_CONTROL_METADATA, diff.path) &&
+ diff.v9 === ENDPOINT_CONTROL_METADATA[diff.path];
+
+const additiveV8ScaffoldingMetadata = (
+ diff: StyleDiff,
+ next: Signature | undefined,
+ isMap: boolean,
+) => {
+ if (diff.v8 !== undefined || !next) return false;
+ const fields = flatten(next.style);
+ if (diff.path === 'theme.colors.gridlineZero') return diff.v9 === fields['theme.colors.gridline'];
+ return (
+ (diff.path === 'theme.colors.mapInteriorBorder' &&
+ diff.v9 === (isMap ? 'rgba(0,0,0,0.25)' : 'rgba(0,0,0,0.14)')) ||
+ (diff.path === 'theme.colors.mapOutlineBorder' &&
+ diff.v9 === (isMap ? 'rgba(0,0,0,0.55)' : 'rgba(0,0,0,0.40)'))
+ );
+};
+
+const underPrefix = (path: string, prefix: string) =>
+ path === prefix || path.startsWith(`${prefix}.`);
+
+/** Whether any spec level (root, layer, override) authors a text annotation. */
+function hasTextAnnotation(value: unknown): boolean {
+ if (!value || typeof value !== 'object') return false;
+ const record = value as Record;
+ if (
+ Array.isArray(record.annotations) &&
+ record.annotations.some((annotation) => (annotation as { type?: string })?.type === 'text')
+ )
+ return true;
+ return Object.values(record).some(hasTextAnnotation);
+}
+
+const results = capture.entries.map((entry) => {
+ const { spec: migrated } = migrateSpecV8(hydrate(entry.spec));
+ let v9: Signature | undefined;
+ let error: string | undefined;
+ try {
+ v9 = signature(compile(migrated, entry.width));
+ } catch (caught) {
+ error = (caught as Error).message;
+ }
+ return {
+ ...entry,
+ v9,
+ error,
+ style: v9 ? styleDiff(entry.signature.style, v9.style) : [],
+ geometry: v9 ? geometryDelta(entry.signature.geometry, v9.geometry) : undefined,
+ };
+});
+const moved = (result: (typeof results)[number]) =>
+ Object.values(result.geometry ?? {}).some((delta) => delta.maxAbs > 0 || delta.structural > 0);
+
+if (process.env.PARITY_REPORT) {
+ const rows = GEOMETRY_CATEGORIES.map((category) => {
+ const deltas = results.map((result) => result.geometry?.[category]);
+ return {
+ category,
+ fixturesMoved: deltas.filter((delta) => delta && (delta.maxAbs > 0 || delta.structural > 0))
+ .length,
+ maxAbsPx: Math.max(0, ...deltas.map((delta) => delta?.maxAbs ?? 0)),
+ structural: deltas.reduce((sum, delta) => sum + (delta?.structural ?? 0), 0),
+ };
+ });
+ console.log(
+ `v8 parity vs ${capture.tag} (${capture.commit.slice(0, 8)}): ${results.length} fixtures, ` +
+ `${results.filter((result) => result.error).length} v9 compile errors, ` +
+ `${results.filter((result) => result.style.length).length} with style mismatches, ` +
+ `${results.filter(moved).length} with geometry diffs, ${capture.skipped.length} skipped at capture`,
+ );
+ console.table(rows);
+ if (process.env.PARITY_DUMP)
+ writeFileSync(process.env.PARITY_DUMP, JSON.stringify(results, null, 1));
+}
+
+describe(`v8 migration parity against ${capture.tag}`, () => {
+ it('has a v8 corpus', () => {
+ expect(new Date(0).getTimezoneOffset()).toBe(0);
+ expect(new Set(capture.entries.map((entry) => entry.name)).size).toBeGreaterThan(200);
+ });
+
+ it('adds only released endpoint control values and narrowly scoped B1 value fonts', () => {
+ expect(VALUE_FONT_REASON).toContain('B1');
+ for (const result of results) {
+ if (!result.v9) continue;
+ const fields = flatten(result.v9.style);
+ for (const [path, value] of Object.entries(ENDPOINT_CONTROL_METADATA))
+ expect(fields[path]).toBe(value);
+ }
+ expect(
+ results.some((result) =>
+ result.style.some((diff) => sanctionedValueFontChange(diff, result.v9)),
+ ),
+ ).toBe(true);
+ });
+
+ it('allowlist only names text-annotated fixtures that move in every listed category', () => {
+ const stale = Object.entries(GEOMETRY_ALLOWLIST).flatMap(([name, { categories }]) => {
+ const mine = results.filter((result) => result.name === name);
+ if (!mine.length) return [`${name}: no such fixture`];
+ if (!hasTextAnnotation(mine[0].spec))
+ return [`${name}: no text annotations, D14 cannot apply`];
+ return categories
+ .filter((category) =>
+ mine.every(({ geometry }) => {
+ const delta = geometry?.[category];
+ return !delta || (delta.maxAbs === 0 && delta.structural === 0);
+ }),
+ )
+ .map((category) => `${name}: ${category} never moves`);
+ });
+ expect(stale).toEqual([]);
+ });
+
+ it('approved geometry exceptions match every exact before/final path value', () => {
+ for (const [key, approval] of [
+ ...Object.entries(APPROVED_C7_ATTRIBUTION),
+ ...Object.entries(APPROVED_B3_MAP_LAYOUT),
+ ...Object.entries(APPROVED_PRE_RFC_LAYOUT),
+ ...Object.entries(APPROVED_RFC_B1_LAYOUT),
+ ...Object.entries(APPROVED_MAP_KEY_PLACEMENT),
+ ...Object.entries(APPROVED_AXIS_TITLE_LAYOUT),
+ ...Object.entries(APPROVED_ENDPOINT_TOP_SWEEP),
+ ]) {
+ const result = results.find((result) => `${result.name} @${result.width}` === key)!;
+ expect(result, key).toBeDefined();
+ expect(approval.reason.length).toBeGreaterThan(20);
+ for (const [category, paths] of Object.entries(approval.changes)) {
+ for (const [path, [before, after]] of Object.entries(paths)) {
+ expect(
+ result.signature.geometry[category][path],
+ `${key} before ${category}.${path}`,
+ ).toBe(before);
+ // Later B3 and C7 reservations can move a previously corrected path.
+ // The new approval still guards the original v8 value and final v9 value.
+ const latestPair =
+ APPROVED_C7_ATTRIBUTION[key]?.changes[category]?.[path] ??
+ APPROVED_B3_MAP_LAYOUT[key]?.changes[category]?.[path];
+ const finalAfter = latestPair ? latestPair[1] : after;
+ expect(result.v9!.geometry[category][path], `${key} after ${category}.${path}`).toBe(
+ finalAfter,
+ );
+ expect(before).not.toBe(after);
+ }
+ }
+ }
+ });
+
+ it('map key corrections match exact before/after style values', () => {
+ for (const [key, approval] of [
+ ...Object.entries(APPROVED_MAP_KEY_PLACEMENT),
+ ...Object.entries(APPROVED_C7_ATTRIBUTION),
+ ...Object.entries(APPROVED_B3_MAP_LAYOUT),
+ ]) {
+ const result = results.find((result) => `${result.name} @${result.width}` === key)!;
+ const before = flatten(result.signature.style);
+ const after = flatten(result.v9!.style);
+ for (const [path, pair] of Object.entries(approval.style)) {
+ expect(before[path]).toBe(pair[0]);
+ expect(after[path]).toBe(pair[1]);
+ expect(pair[0]).not.toBe(pair[1]);
+ }
+ }
+ });
+
+ it('style allowlist only names prefixes and categories that actually differ', () => {
+ const stale = Object.entries(STYLE_ALLOWLIST).flatMap(([name, entry]) => {
+ const mine = results.filter((result) => result.name === name);
+ if (!mine.length) return [`${name}: no such fixture`];
+ return [
+ ...entry.style
+ .filter((prefix) =>
+ mine.every(({ style }) => !style.some((diff) => underPrefix(diff.path, prefix))),
+ )
+ .map((prefix) => `${name}: style ${prefix} never differs`),
+ ...entry.geometry
+ .filter((category) =>
+ mine.every(({ geometry }) => {
+ const delta = geometry?.[category];
+ return !delta || (delta.maxAbs === 0 && delta.structural === 0);
+ }),
+ )
+ .map((category) => `${name}: ${category} never moves`),
+ ];
+ });
+ expect(stale).toEqual([]);
+ });
+
+ for (const result of results) {
+ describe(`${result.name} @${result.width}`, () => {
+ it('compiles after migration', () => {
+ expect(result.error).toBeUndefined();
+ });
+ it('keeps v8 styling', () => {
+ const allowed = STYLE_ALLOWLIST[result.name]?.style ?? [];
+ expect(
+ result.style.filter(
+ (diff) =>
+ !allowed.some((prefix) => underPrefix(diff.path, prefix)) &&
+ !CORPUS_STYLE_CHANGES.some(({ pattern }) => pattern.test(diff.path)) &&
+ !additiveEndpointMetadata(diff) &&
+ !additiveV8ScaffoldingMetadata(
+ diff,
+ result.v9,
+ (result.spec as { type?: string }).type === 'map',
+ ) &&
+ !sanctionedValueFontChange(diff, result.v9) &&
+ !sanctionedEndpointPrecisionChange(diff) &&
+ !Object.entries({
+ ...APPROVED_MAP_KEY_PLACEMENT[`${result.name} @${result.width}`]?.style,
+ ...APPROVED_B3_MAP_LAYOUT[`${result.name} @${result.width}`]?.style,
+ ...APPROVED_C7_ATTRIBUTION[`${result.name} @${result.width}`]?.style,
+ }).some(
+ ([path, pair]) => path === diff.path && pair[0] === diff.v8 && pair[1] === diff.v9,
+ ),
+ ),
+ ).toEqual([]);
+ });
+ it('keeps v8 geometry outside D14-allowlisted categories', () => {
+ const allowed = new Set([
+ ...(GEOMETRY_ALLOWLIST[result.name]?.categories ?? []),
+ ...(STYLE_ALLOWLIST[result.name]?.geometry ?? []),
+ ]);
+ const approved = [
+ APPROVED_C7_ATTRIBUTION[`${result.name} @${result.width}`],
+ APPROVED_B3_MAP_LAYOUT[`${result.name} @${result.width}`],
+ APPROVED_PRE_RFC_LAYOUT[`${result.name} @${result.width}`],
+ APPROVED_RFC_B1_LAYOUT[`${result.name} @${result.width}`],
+ APPROVED_MAP_KEY_PLACEMENT[`${result.name} @${result.width}`],
+ APPROVED_AXIS_TITLE_LAYOUT[`${result.name} @${result.width}`],
+ APPROVED_ENDPOINT_TOP_SWEEP[`${result.name} @${result.width}`],
+ ];
+ const unexpected: Record = {};
+ for (const [category, delta] of Object.entries(result.geometry ?? {})) {
+ if (allowed.has(category as Category) || (!delta.maxAbs && !delta.structural)) continue;
+ const before = result.signature.geometry[category];
+ const after = result.v9!.geometry[category];
+ for (const path of new Set([...Object.keys(before), ...Object.keys(after)])) {
+ if (before[path] === after[path]) continue;
+ const matches = approved.some((approval) => {
+ const pair = approval?.changes[category]?.[path];
+ return pair && pair[0] === before[path] && pair[1] === after[path];
+ });
+ if (!matches) {
+ unexpected[`${category}.${path}`] = { before: before[path], after: after[path] };
+ }
+ }
+ }
+ expect(unexpected).toEqual({});
+ });
+ });
+ }
+});
diff --git a/examples/src/__tests__/parity-signature.ts b/examples/src/__tests__/parity-signature.ts
new file mode 100644
index 00000000..3faa2707
--- /dev/null
+++ b/examples/src/__tests__/parity-signature.ts
@@ -0,0 +1,516 @@
+/**
+ * Layout signature for the v8 -> v9 migration parity test.
+ *
+ * Shared verbatim by `scripts/capture-v8-parity.mjs` (which copies this file into
+ * a temporary v8.9.1 worktree) and `migration-v8-parity.test.ts`, so both sides
+ * extract with identical code. Keep it dependency-free: it reads layouts loosely
+ * because it runs against two engine versions and every compiled layout kind
+ * (chart/layer/facet, table, graph, sankey, tilemap, geo map, barlist).
+ *
+ * Field mapping 8.9.1 -> v9: the layout types are identical for everything read
+ * here. v9 adds `AnnotationFootnote.subtitle/spans` (not read; notes compare
+ * `index`, `text` and `lines`) and drops `TableLayout.compact` (not read).
+ *
+ * - `style`: everything the RFC promises migration keeps (fonts, colors, text,
+ * weights, mark paint, annotation ink, connectors and arrows). Compared exactly.
+ * - `geometry`: flattened `category -> path -> value` maps. Numbers are rounded to
+ * 0.01px; strings (tick labels, note text) are content whose placement depends
+ * on layout, so they count as geometry differences rather than style.
+ */
+
+// biome-ignore lint/suspicious/noExplicitAny: layouts are read loosely across two engine versions.
+type Any = any;
+
+export type GeometryValue = number | string | null;
+export type Geometry = Record>;
+export interface Signature {
+ style: Record;
+ geometry: Geometry;
+}
+
+export const GEOMETRY_CATEGORIES = [
+ 'dims',
+ 'area',
+ 'chrome',
+ 'axes',
+ 'marks',
+ 'labels',
+ 'annotations',
+ 'legend',
+ 'endpointLabels',
+ 'notes',
+] as const;
+
+const round = (value: unknown, places: number): unknown => {
+ if (typeof value === 'number') {
+ if (!Number.isFinite(value)) return String(value);
+ const factor = 10 ** places;
+ const rounded = Math.round(value * factor) / factor;
+ return Object.is(rounded, -0) ? 0 : rounded;
+ }
+ if (Array.isArray(value)) return value.map((item) => round(item, places));
+ if (value && typeof value === 'object') {
+ if (value instanceof Map) return round(Object.fromEntries(value), places);
+ const out: Record = {};
+ for (const key of Object.keys(value).sort()) {
+ const child = (value as Record)[key];
+ if (child !== undefined && typeof child !== 'function') out[key] = round(child, places);
+ }
+ return out;
+ }
+ return value === undefined ? null : value;
+};
+const clean = (value: unknown) => round(value, 3);
+
+const textStyle = (style: Any) =>
+ style
+ ? clean({
+ fontFamily: style.fontFamily,
+ fontSize: style.fontSize,
+ fontWeight: style.fontWeight,
+ fill: style.fill,
+ lineHeight: style.lineHeight,
+ fontVariant: style.fontVariant,
+ })
+ : null;
+
+const connectorStyle = (connector: Any) =>
+ connector
+ ? {
+ style: connector.style ?? null,
+ arrow: connector.arrow ?? null,
+ stroke: connector.stroke ?? null,
+ }
+ : null;
+
+const labelStyle = (label: Any) =>
+ label
+ ? {
+ text: label.text ?? null,
+ style: textStyle(label.style),
+ background: label.background ?? null,
+ halo: label.halo ?? null,
+ connector: connectorStyle(label.connector),
+ }
+ : null;
+
+function markStyle(mark: Any) {
+ return clean({
+ type: mark.type ?? null,
+ key: mark.key ?? mark.nodeId ?? mark.stateCode ?? mark.id ?? null,
+ series: mark.seriesKey ?? null,
+ fill: mark.fill ?? mark.bar?.fill ?? null,
+ stroke: mark.stroke ?? null,
+ strokeWidth: mark.strokeWidth ?? null,
+ strokeDasharray: mark.strokeDasharray ?? null,
+ opacity: mark.opacity ?? null,
+ fillOpacity: mark.fillOpacity ?? null,
+ strokeOpacity: mark.strokeOpacity ?? null,
+ pattern: mark.pattern ?? null,
+ cornerRadius: mark.cornerRadius ?? mark.bar?.cornerRadius ?? null,
+ sourceColor: mark.sourceColor ?? null,
+ targetColor: mark.targetColor ?? null,
+ noData: mark.noData ?? null,
+ text: mark.type === 'textMark' ? mark.text : null,
+ font:
+ mark.type === 'textMark'
+ ? { fontSize: mark.fontSize, fontWeight: mark.fontWeight, fontFamily: mark.fontFamily }
+ : null,
+ label: labelStyle(mark.label),
+ subtitle: labelStyle(mark.subtitle),
+ valueLabel: mark.valueLabel
+ ? { text: mark.valueLabel.text ?? null, style: textStyle(mark.valueLabel.style) }
+ : null,
+ });
+}
+
+/** Bounding box of the first two coordinates of every path command (no arcs). */
+function pathBox(path: unknown) {
+ if (typeof path !== 'string' || /[aA]/.test(path)) return null;
+ const numbers = path.match(/-?\d*\.?\d+(?:e-?\d+)?/g)?.map(Number) ?? [];
+ const xs: number[] = [];
+ const ys: number[] = [];
+ for (let i = 0; i + 1 < numbers.length; i += 2) {
+ xs.push(numbers[i]);
+ ys.push(numbers[i + 1]);
+ }
+ return pointsBox(xs.map((x, i) => ({ x, y: ys[i] })));
+}
+
+function pointsBox(points: Any[] | undefined) {
+ const finite = (points ?? []).filter(
+ (point) => point && Number.isFinite(point.x) && Number.isFinite(point.y),
+ );
+ if (!finite.length) return null;
+ const xs = finite.map((point) => point.x);
+ const ys = finite.map((point) => point.y);
+ const x = Math.min(...xs);
+ const y = Math.min(...ys);
+ return { x, y, width: Math.max(...xs) - x, height: Math.max(...ys) - y };
+}
+
+function markBounds(mark: Any): Record | null {
+ switch (mark.type) {
+ case 'rect':
+ case 'sankeyNode':
+ return { x: mark.x, y: mark.y, width: mark.width, height: mark.height };
+ case 'point':
+ case 'map-point':
+ return { cx: mark.cx, cy: mark.cy, r: mark.r };
+ case 'line':
+ return pointsBox(mark.points) ?? pathBox(mark.path);
+ case 'area':
+ return pointsBox([...(mark.topPoints ?? []), ...(mark.bottomPoints ?? [])]);
+ case 'arc':
+ return {
+ cx: mark.center?.x,
+ cy: mark.center?.y,
+ innerRadius: mark.innerRadius,
+ outerRadius: mark.outerRadius,
+ startAngle: mark.startAngle,
+ endAngle: mark.endAngle,
+ };
+ case 'textMark':
+ return { x: mark.x, y: mark.y };
+ case 'rule':
+ return { x1: mark.x1, y1: mark.y1, x2: mark.x2, y2: mark.y2 };
+ case 'tick':
+ return { x: mark.x, y: mark.y, length: mark.length };
+ case 'sankeyLink':
+ return { width: mark.width, ...pathBox(mark.path) };
+ case 'tile':
+ return { x: mark.x, y: mark.y, size: mark.size };
+ case 'map-feature':
+ return mark.bounds ?? null;
+ case 'barlist-row':
+ return { bar: mark.bar, track: mark.track, y: mark.y, height: mark.height };
+ default:
+ return null;
+ }
+}
+
+function legendsOf(layout: Any): Array<[string, Any]> {
+ const named: Array<[string, Any]> = [];
+ if (Array.isArray(layout.legends) && layout.legends.length)
+ for (const [i, legend] of layout.legends.entries()) named.push([`legends.${i}`, legend]);
+ else if (layout.legend) named.push(['legend', layout.legend]);
+ for (const key of [
+ 'gradientLegend',
+ 'categoricalLegend',
+ 'continuousLegend',
+ 'pointCategoricalLegend',
+ 'pointContinuousLegend',
+ 'pointSizeLegend',
+ ])
+ if (layout[key]) named.push([key, layout[key]]);
+ return named;
+}
+
+function legendStyle(legend: Any) {
+ return clean({
+ type: legend.type ?? 'categorical',
+ channel: legend.channel ?? null,
+ position: legend.position ?? null,
+ labelStyle: textStyle(legend.labelStyle),
+ titleStyle: textStyle(legend.titleStyle),
+ title: legend.title ?? null,
+ entries: (legend.entries ?? []).map((entry: Any) => ({
+ label: entry.label,
+ color: entry.color,
+ shape: entry.shape,
+ active: entry.active ?? null,
+ overflow: entry.overflow ?? null,
+ })),
+ swatchChipFill: legend.swatchChipFill ?? null,
+ colorStops: legend.colorStops ?? null,
+ bins: (legend.bins ?? []).map((bin: Any) => bin.color),
+ ticks: (legend.ticks ?? []).map((tick: Any) => tick.label),
+ circles: (legend.circles ?? []).map((circle: Any) => circle.label),
+ minLabel: legend.minLabel ?? null,
+ maxLabel: legend.maxLabel ?? null,
+ stroke: legend.stroke ?? null,
+ noData: legend.noData
+ ? { fill: legend.noData.fill, label: legend.noData.label, pattern: legend.noData.pattern }
+ : null,
+ });
+}
+
+function annotationStyle(annotation: Any) {
+ return clean({
+ type: annotation.type,
+ id: annotation.id ?? null,
+ label: labelStyle(annotation.label),
+ subtitle: annotation.subtitle
+ ? { text: annotation.subtitle.text, style: textStyle(annotation.subtitle.style) }
+ : null,
+ fill: annotation.fill ?? null,
+ stroke: annotation.stroke ?? null,
+ opacity: annotation.opacity ?? null,
+ strokeDasharray: annotation.strokeDasharray ?? null,
+ strokeWidth: annotation.strokeWidth ?? null,
+ dot: annotation.dot
+ ? {
+ fill: annotation.dot.fill,
+ stroke: annotation.dot.stroke,
+ strokeWidth: annotation.dot.strokeWidth,
+ radius: annotation.dot.radius,
+ }
+ : null,
+ });
+}
+
+const annotationKey = (annotation: Any, index: number) =>
+ `a${annotation.specIndex ?? `#${index}`}${annotation.id ? `:${annotation.id}` : ''}`;
+
+function axesStyle(axes: Any) {
+ const out: Record = {};
+ for (const name of ['x', 'y', 'y2']) {
+ const axis = axes?.[name];
+ if (!axis) continue;
+ out[name] = {
+ title: axis.label ?? null,
+ titleStyle: textStyle(axis.labelStyle),
+ tickStyle: textStyle(axis.tickLabelStyle),
+ };
+ }
+ return out;
+}
+
+const CHROME_KEYS = ['eyebrow', 'title', 'subtitle', 'source', 'byline', 'footer', 'brand'];
+
+/** Extract the parity signature from any compiled layout. */
+export function signature(layout: Any): Signature {
+ const geometry: Geometry = {};
+ for (const category of GEOMETRY_CATEGORIES) geometry[category] = {};
+ const put = (category: string, path: string, value: unknown) => {
+ const rounded = round(value, 2);
+ if (rounded && typeof rounded === 'object') {
+ for (const [key, child] of Object.entries(rounded as Record))
+ put(category, `${path}.${key}`, child);
+ return;
+ }
+ geometry[category][path] = rounded as GeometryValue;
+ };
+
+ const theme = layout.theme ?? {};
+ const style: Record = {
+ theme: clean({
+ colors: theme.colors,
+ fonts: theme.fonts,
+ chrome: theme.chrome,
+ rule: theme.rule,
+ spacing: theme.spacing,
+ borderRadius: theme.borderRadius,
+ isDark: theme.isDark,
+ seriesStrategy: theme.seriesStrategy,
+ }),
+ };
+
+ // Dimensions and plot area.
+ put('dims', 'size', layout.dimensions ?? { width: layout.width, height: layout.height });
+ if (layout.mapSize) put('dims', 'mapSize', layout.mapSize);
+ if (layout.area) put('area', 'rect', layout.area);
+
+ // Chrome.
+ const chrome = layout.chrome ?? {};
+ const chromeStyle: Record = {};
+ for (const key of CHROME_KEYS) {
+ const element = chrome[key];
+ if (!element) continue;
+ chromeStyle[key] = { text: element.text, style: textStyle(element.style) };
+ put('chrome', key, { x: element.x, y: element.y, maxWidth: element.maxWidth });
+ }
+ if (chrome.rule) {
+ chromeStyle.rule = clean({ color: chrome.rule.color, thickness: chrome.rule.thickness });
+ put('chrome', 'rule', { x: chrome.rule.x, y: chrome.rule.y, width: chrome.rule.width });
+ }
+ chromeStyle.seriesSummary = clean(
+ (chrome.seriesSummary ?? []).map((entry: Any) => ({ key: entry.key, color: entry.color })),
+ );
+ style.chrome = chromeStyle;
+ for (const key of ['topHeight', 'bottomHeight', 'noteBandHeight', 'grownHeight', 'bottomAnchorY'])
+ if (chrome[key] !== undefined) put('chrome', key, chrome[key]);
+ (chrome.footnotes ?? []).forEach((note: Any, i: number) => {
+ put('notes', `${i}.index`, note.index);
+ put('notes', `${i}.text`, note.text);
+ put('notes', `${i}.lines`, (note.lines ?? []).join('\n'));
+ });
+ for (const [i, entry] of (chrome.seriesSummary ?? []).entries())
+ put('notes', `summary.${i}`, (entry.lines ?? [entry.text]).join('\n'));
+
+ // Axes, marks, annotations: the chart body and every facet panel.
+ const bodies: Array<[string, Any]> = [['', layout]];
+ for (const panel of layout.facet?.panels ?? []) bodies.push([`facet[${panel.key}].`, panel]);
+ if (layout.facet) {
+ style.facetHeaders = clean(
+ layout.facet.panels.map((panel: Any) => ({
+ text: panel.header?.text,
+ fontSize: panel.header?.fontSize,
+ fontWeight: panel.header?.fontWeight,
+ })),
+ );
+ for (const panel of layout.facet.panels) {
+ put('area', `facet[${panel.key}]`, panel.area);
+ put('chrome', `facet[${panel.key}].header`, { x: panel.header?.x, y: panel.header?.y });
+ }
+ }
+
+ const axesStyles: Record = {};
+ const markStyles: Record = {};
+ const annotationStyles: Record = {};
+ for (const [prefix, body] of bodies) {
+ if (body.axes) {
+ axesStyles[prefix || 'root'] = axesStyle(body.axes);
+ for (const name of ['x', 'y', 'y2']) {
+ const axis = body.axes[name];
+ if (!axis) continue;
+ const at = `${prefix}${name}`;
+ put('axes', `${at}.start`, axis.start);
+ put('axes', `${at}.end`, axis.end);
+ put('axes', `${at}.tickAngle`, axis.tickAngle ?? 0);
+ put('axes', `${at}.titlePosition`, axis.titlePosition ?? null);
+ put('axes', `${at}.gridlines`, axis.gridlines?.length ?? 0);
+ (axis.ticks ?? []).forEach((tick: Any, i: number) => {
+ put('axes', `${at}.tick.${i}.position`, tick.position);
+ put('axes', `${at}.tick.${i}.label`, tick.label);
+ });
+ }
+ }
+
+ const marks: Any[] = [
+ ...(body.marks ?? []),
+ ...(prefix ? [] : [...(layout.nodes ?? []), ...(layout.links ?? [])]),
+ ...(prefix ? [] : [...(layout.tiles ?? []), ...(layout.features ?? [])]),
+ ...(prefix ? [] : [...(layout.pointMarks ?? []), ...(layout.rows ?? [])]),
+ ].filter((mark) => mark && typeof mark === 'object' && typeof mark.type === 'string');
+ marks.forEach((mark, i) => {
+ const at = `${prefix}${i}`;
+ markStyles[at] = markStyle(mark);
+ const bounds = markBounds(mark);
+ if (bounds) put('marks', at, bounds);
+ for (const key of ['label', 'subtitle', 'valueLabel'])
+ if (mark[key] && typeof mark[key].x === 'number')
+ put('labels', `${at}.${key}`, { x: mark[key].x, y: mark[key].y });
+ });
+
+ (body.annotations ?? []).forEach((annotation: Any, i: number) => {
+ const at = `${prefix}${annotationKey(annotation, i)}`;
+ annotationStyles[at] = annotationStyle(annotation);
+ if (annotation.label) {
+ put('annotations', `${at}.label`, { x: annotation.label.x, y: annotation.label.y });
+ put('annotations', `${at}.visible`, annotation.label.visible ? 1 : 0);
+ put('annotations', `${at}.lines`, (annotation.label.lines ?? []).join('\n'));
+ const connector = annotation.label.connector;
+ if (connector)
+ put('annotations', `${at}.connector`, { from: connector.from, to: connector.to });
+ }
+ if (annotation.subtitle)
+ put('annotations', `${at}.subtitle`, {
+ x: annotation.subtitle.x,
+ y: annotation.subtitle.y,
+ });
+ if (annotation.rect) put('annotations', `${at}.rect`, annotation.rect);
+ if (annotation.line) put('annotations', `${at}.line`, annotation.line);
+ if (annotation.dot)
+ put('annotations', `${at}.dot`, { x: annotation.dot.x, y: annotation.dot.y });
+ });
+ }
+ style.axes = axesStyles;
+ style.marks = markStyles;
+ style.annotations = annotationStyles;
+
+ // Legends.
+ const legendStyles: Record = {};
+ for (const [name, legend] of legendsOf(layout)) {
+ legendStyles[name] = legendStyle(legend);
+ if (legend.bounds) put('legend', `${name}.bounds`, legend.bounds);
+ for (const [i, entry] of (legend.entryPositions ?? []).entries())
+ put('legend', `${name}.entry.${i}`, { x: entry.x, y: entry.y });
+ if (legend.bar) put('legend', `${name}.bar`, legend.bar);
+ }
+ style.legends = legendStyles;
+
+ // Endpoint labels.
+ const endpoint = layout.endpointLabels;
+ if (endpoint) {
+ const entryStyle = (entry: Any) => ({
+ series: entry.seriesKey,
+ label: (entry.labelLines ?? []).join(' '),
+ value: entry.value,
+ color: entry.color,
+ marker: entry.marker
+ ? {
+ fill: entry.marker.fill,
+ stroke: entry.marker.stroke,
+ strokeWidth: entry.marker.strokeWidth,
+ radius: entry.marker.radius,
+ }
+ : null,
+ });
+ style.endpointLabels = clean({
+ labelStyle: textStyle(endpoint.labelStyle),
+ valueStyle: textStyle(endpoint.valueStyle),
+ swatchChipFill: endpoint.swatchChipFill,
+ entries: (endpoint.entries ?? []).map(entryStyle),
+ leading: (endpoint.leading ?? []).map(entryStyle),
+ });
+ put('endpointLabels', 'bounds', endpoint.bounds);
+ if (endpoint.leadingBounds) put('endpointLabels', 'leadingBounds', endpoint.leadingBounds);
+ (endpoint.entries ?? []).forEach((entry: Any, i: number) => {
+ put('endpointLabels', `${i}.labelY`, entry.labelY);
+ put('endpointLabels', `${i}.lines`, (entry.labelLines ?? []).join('\n'));
+ });
+ }
+
+ // Tables: paint per cell, widths as geometry.
+ if (Array.isArray(layout.columns)) {
+ style.table = clean({
+ density: layout.density,
+ striped: layout.striped,
+ columns: layout.columns.map((column: Any) => ({
+ key: column.key,
+ label: column.label,
+ cellType: column.cellType,
+ align: column.align,
+ })),
+ rows: (layout.rows ?? []).map((row: Any) =>
+ (row.cells ?? []).map((cell: Any) => [cell.formattedValue, cell.style ?? null]),
+ ),
+ });
+ for (const column of layout.columns) put('marks', `column.${column.key}`, column.width);
+ }
+
+ // Graphs: nodes and edges carry no positions until the force simulation runs.
+ if (Array.isArray(layout.edges)) {
+ style.graph = clean({
+ nodes: (layout.nodes ?? []).map((node: Any) => ({
+ id: node.id,
+ fill: node.fill,
+ stroke: node.stroke,
+ strokeWidth: node.strokeWidth,
+ opacity: node.opacity,
+ label: node.label ?? null,
+ })),
+ edges: layout.edges.map((edge: Any) => ({
+ stroke: edge.stroke,
+ strokeWidth: edge.strokeWidth,
+ style: edge.style,
+ })),
+ });
+ for (const node of layout.nodes ?? []) put('marks', `node.${node.id}.radius`, node.radius);
+ }
+
+ return { style: clean(style) as Record, geometry };
+}
+
+/** Flatten a nested value into `path -> leaf` for diff reporting. */
+export function flatten(value: unknown, path = '', out: Record = {}) {
+ if (value && typeof value === 'object') {
+ const entries = Array.isArray(value)
+ ? value.map((child, i) => [String(i), child] as const)
+ : Object.entries(value);
+ if (!entries.length) out[path] = Array.isArray(value) ? '[]' : '{}';
+ for (const [key, child] of entries) flatten(child, path ? `${path}.${key}` : key, out);
+ } else out[path] = value;
+ return out;
+}
diff --git a/examples/src/__tests__/v9-editorial-examples.test.ts b/examples/src/__tests__/v9-editorial-examples.test.ts
new file mode 100644
index 00000000..7f5bd2bd
--- /dev/null
+++ b/examples/src/__tests__/v9-editorial-examples.test.ts
@@ -0,0 +1,100 @@
+///
+import type { ChartSpec, LineMark, RectMark } from '@opendata-ai/openchart-core';
+import { describe, expect, it } from 'vitest';
+import { compileChart } from '../../../packages/engine/src/compile';
+
+const modules = import.meta.glob(
+ [
+ '../gallery/charts-bar-column.stories.tsx',
+ '../gallery/charts-line-area.stories.tsx',
+ '../gallery/features-annotations.stories.tsx',
+ '../gallery/showcase.stories.tsx',
+ '../gallery/welcome.stories.tsx',
+ '../testing/fixtures-bars.stories.tsx',
+ ],
+ { eager: true },
+) as Record }>;
+const specFor = (file: string, name: string) =>
+ modules[`../${file}.stories.tsx`].__authoredValues[name];
+
+const populationExamples = [
+ ['gallery/charts-bar-column', 'simpleBarsSpec'],
+ ['gallery/showcase', 'fullChromeSpec'],
+ ['testing/fixtures-bars', 'populationBarSpec'],
+] as const;
+const amazonExamples = [
+ ['gallery/charts-line-area', 'fiveSeriesSpec'],
+ ['gallery/showcase', 'multiLineSpec'],
+ ['gallery/welcome', 'heroSpec'],
+] as const;
+
+describe('v9 authored editorial choices', () => {
+ it.each(
+ populationExamples,
+ )('%s uses flat ranked population bars with explicit India/China emphasis', (file, name) => {
+ const original = specFor(file, name);
+ const spec = { ...original, data: [...original.data].reverse() } as ChartSpec;
+ const before = JSON.stringify(spec.data);
+ expect(spec.encoding.color).toMatchObject({ field: 'country', highlight: ['India', 'China'] });
+ const layout = compileChart(spec, { width: 700, height: 500, onWarn: () => {} });
+ expect(JSON.stringify(spec.data)).toBe(before);
+ const bars = layout.marks
+ .filter((mark): mark is RectMark => mark.type === 'rect')
+ .sort((a, b) => a.y - b.y);
+ expect(bars.map((bar) => bar.data.population)).toEqual(
+ [...bars.map((bar) => Number(bar.data.population))].sort((a, b) => b - a),
+ );
+ expect(bars.map((bar) => bar.data.country).slice(0, 2)).toEqual(['India', 'China']);
+ expect(bars.every((bar) => typeof bar.fill === 'string')).toBe(true);
+ expect(bars[0].fill).not.toBe(bars[2].fill);
+ expect(bars[1].fill).not.toBe(bars[2].fill);
+ });
+
+ it.each(
+ amazonExamples,
+ )('%s explicitly focuses Amazon and labels every comparison series', (file, name) => {
+ const spec = specFor(file, name);
+ expect(spec.encoding.color).toMatchObject({
+ field: 'company',
+ scale: { domain: ['Amazon', 'Apple', 'Alphabet', 'Microsoft', 'Meta'] },
+ });
+ expect(spec.seriesStyles).toMatchObject({ Amazon: { strokeWidth: 3 } });
+ for (const width of [390, 1280]) {
+ const layout = compileChart(spec, { width, height: 460, onWarn: () => {} });
+ const lines = layout.marks.filter((mark): mark is LineMark => mark.type === 'line');
+ const amazon = lines.find((line) => line.seriesKey === 'Amazon')!;
+ expect(lines).toHaveLength(5);
+ expect(
+ lines
+ .filter((line) => line.seriesKey !== 'Amazon')
+ .every((line) => line.stroke !== amazon.stroke && line.strokeWidth < amazon.strokeWidth),
+ ).toBe(true);
+ expect(layout.endpointLabels?.entries.map((entry) => entry.seriesKey).sort()).toEqual([
+ 'Alphabet',
+ 'Amazon',
+ 'Apple',
+ 'Meta',
+ 'Microsoft',
+ ]);
+ }
+ });
+
+ it('uses rule-relative placement instead of pixel offsets in reference-line examples', () => {
+ for (const spec of [
+ specFor('gallery/charts-bar-column', 'simpleBarsSpec'),
+ specFor('gallery/showcase', 'fullChromeSpec'),
+ specFor('testing/fixtures-bars', 'populationBarSpec'),
+ specFor('gallery/charts-bar-column', 'columnsSpec'),
+ specFor('gallery/features-annotations', 'reflineSpec'),
+ ]) {
+ const rules = spec.annotations!.filter(
+ (annotation) => annotation.type === 'refline' && annotation.label,
+ );
+ expect(rules.length).toBeGreaterThan(0);
+ for (const rule of rules) {
+ expect(rule).not.toHaveProperty('labelOffset');
+ expect(rule).toHaveProperty('labelPlacement');
+ }
+ }
+ });
+});
diff --git a/examples/src/charts/mobile-regression.stories.tsx b/examples/src/charts/mobile-regression.stories.tsx
index 4feb8fc4..bc1e7b8f 100644
--- a/examples/src/charts/mobile-regression.stories.tsx
+++ b/examples/src/charts/mobile-regression.stories.tsx
@@ -468,3 +468,58 @@ export const ChromeMaxLines = () => (
);
+
+// Outside reference labels in a short, narrow figure (v9 review, Oct 2026).
+// Three wrapped event labels plus a wrapped threshold label cannot all fit in
+// the lanes above and beside a 400px-tall plot at ~358px wide. The top lane
+// packs labels whose extents don't overlap into one row (2017 and 2022 share
+// it), takes at most 25% of the height, and yields before chrome: at 400px the
+// subtitle stays, one row sits above the plot, and the 2020 label falls back
+// inside the plot on its own. Every label stays on canvas.
+const referenceLaneOverflowSpec: ChartSpec = {
+ mark: 'line',
+ data: [2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024].map((year, i) => ({
+ year: String(year),
+ rate: 4 + Math.sin(i / 1.5) * 1.6 + i * 0.25,
+ })),
+ encoding: {
+ x: { field: 'year', type: 'temporal', axis: { title: undefined } },
+ y: { field: 'rate', type: 'quantitative', axis: { title: 'Unemployment rate (%)' } },
+ },
+ chrome: {
+ title: 'Three policy changes, one labor market',
+ subtitle: 'Unemployment rate around each policy event',
+ },
+ annotations: [
+ {
+ type: 'refline',
+ x: '2017',
+ label: 'Minimum wage increase takes effect statewide',
+ labelPlacement: 'outside',
+ },
+ {
+ type: 'refline',
+ x: '2020',
+ label: 'Pandemic emergency declaration and closures',
+ labelPlacement: 'outside',
+ },
+ {
+ type: 'refline',
+ x: '2022',
+ label: 'Expanded child tax credit expires nationwide',
+ labelPlacement: 'outside',
+ },
+ {
+ type: 'refline',
+ y: 6,
+ label: 'Long-run average across the full period',
+ labelPlacement: 'outside',
+ },
+ ],
+};
+
+export const ReferenceLaneOverflow = () => (
+
+
+
+);
diff --git a/examples/src/gallery/attribution.stories.tsx b/examples/src/gallery/attribution.stories.tsx
new file mode 100644
index 00000000..f32ff9ae
--- /dev/null
+++ b/examples/src/gallery/attribution.stories.tsx
@@ -0,0 +1,139 @@
+import type { Chrome } from '@opendata-ai/openchart-core';
+import {
+ BarList,
+ Chart,
+ DataTable,
+ GeoMap,
+ Graph,
+ Sankey,
+ TileMap,
+} from '@opendata-ai/openchart-react';
+import usStates from 'us-atlas/states-10m.json';
+
+export default { title: 'Gallery / Attribution' };
+const values = [
+ { name: 'North', value: 42 },
+ { name: 'South', value: 31 },
+ { name: 'West', value: 17 },
+];
+function Matrix({ custom = false, large = false }: { custom?: boolean; large?: boolean }) {
+ const chrome: Chrome = {
+ source: {
+ text: 'Source: County research office and administrative records, 2020–2025. Values are illustrative.',
+ ...(large ? { style: { fontSize: 24 } } : {}),
+ },
+ byline: { text: 'Analysis: Open research team', ...(large ? { style: { fontSize: 24 } } : {}) },
+ footer: 'Methods and coverage remain visible alongside the attribution.',
+ ...(custom
+ ? { brand: { text: 'Local research\nOpen methods', style: { fontSize: large ? 28 : 16 } } }
+ : {}),
+ };
+ const card = { width: '100%', height: large ? 650 : 500 };
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
+export const AllVisualizations = () => ;
+export const CustomBrand = () => ;
+export const LargeText = () => ;
diff --git a/examples/src/gallery/charts-bar-column.stories.tsx b/examples/src/gallery/charts-bar-column.stories.tsx
index 11a6f427..4bc81402 100644
--- a/examples/src/gallery/charts-bar-column.stories.tsx
+++ b/examples/src/gallery/charts-bar-column.stories.tsx
@@ -30,14 +30,24 @@ const ACCENT = '#0e7490';
const simpleBarsSpec: ChartSpec = {
animation: true,
- mark: { type: 'bar', fill: hBarGradient(ACCENT) },
+ mark: 'bar',
data: [...populationByCountry.data],
encoding: {
x: { field: 'population', type: 'quantitative', axis: { title: 'Population', format: '.2~s' } },
- y: { field: 'country', type: 'nominal' },
+ y: { field: 'country', type: 'nominal', sort: 'x' },
+ color: { field: 'country', type: 'nominal', highlight: ['India', 'China'] },
},
+ legend: { show: false },
+ labels: { color: 'var(--oc-text)' },
annotations: [
- { type: 'refline', x: 1_000_000_000, label: '1 billion', style: 'dashed', stroke: '#cc4444' },
+ {
+ type: 'refline',
+ x: 1_000_000_000,
+ label: '1 billion',
+ labelPlacement: 'outside',
+ style: 'dashed',
+ stroke: '#cc4444',
+ },
],
chrome: {
title: 'India Has Overtaken China as the Most Populous Country',
@@ -76,7 +86,7 @@ const columnsSpec: ChartSpec = {
style: 'dashed',
stroke: '#64748b',
strokeWidth: 1,
- labelOffset: { dx: -560, dy: -10 },
+ labelPlacement: 'outside',
},
],
labels: { density: 'all', format: ',.0f' },
@@ -489,3 +499,140 @@ export const BarAndColumn = () => (
);
+
+/** Explicit candidates for the desktop craft review; established defaults stay intact. */
+export const ValuePlacement = () => {
+ const ranked: ChartSpec = {
+ mark: { type: 'bar', size: 16 },
+ data: [
+ { name: 'Research', value: 128.4 },
+ { name: 'Operations', value: 75.2 },
+ { name: 'Training', value: 8.6 },
+ { name: 'Contingency', value: 0 },
+ { name: 'Adjustments', value: -24.3 },
+ ],
+ encoding: {
+ x: { field: 'value', type: 'quantitative', axis: false },
+ y: {
+ field: 'name',
+ type: 'nominal',
+ sort: null,
+ scale: { paddingInner: 0.35 },
+ axis: { grid: false },
+ },
+ },
+ labels: {
+ placement: 'column',
+ density: 'all',
+ format: '.1f',
+ suffix: 'M',
+ color: 'var(--oc-text)',
+ },
+ legend: { show: false },
+ chrome: {
+ title: 'Every budget adjustment has a place',
+ subtitle: 'Illustrative budget allocations, millions',
+ },
+ };
+ const signed: ChartSpec = {
+ ...ranked,
+ mark: { type: 'bar', size: 28 },
+ encoding: {
+ x: { field: 'name', type: 'nominal', sort: null, scale: { paddingInner: 0.4 } },
+ y: { field: 'value', type: 'quantitative', axis: { title: 'Millions' } },
+ },
+ labels: {
+ placement: 'outside',
+ density: 'all',
+ format: '.1f',
+ suffix: 'M',
+ color: 'var(--oc-text)',
+ },
+ chrome: {
+ title: 'Positive and negative values follow their endpoints',
+ subtitle: 'The same observations, with explicit outside labels',
+ },
+ };
+ return (
+
+
+
+ );
+};
+
+export const FacetedValues = () => (
+
+
+
+);
diff --git a/examples/src/gallery/charts-line-area.stories.tsx b/examples/src/gallery/charts-line-area.stories.tsx
index f410dc87..4c0f10df 100644
--- a/examples/src/gallery/charts-line-area.stories.tsx
+++ b/examples/src/gallery/charts-line-area.stories.tsx
@@ -146,7 +146,7 @@ const multiSeriesSpec: ChartSpec = {
opacity: 0.06,
},
],
- labels: { density: 'endpoints', format: '.1f' },
+ endpointLabels: { format: '+.1f' },
chrome: {
title: 'Three Economies, Three Recoveries',
subtitle: 'Annual real GDP growth rate, 2018-2024',
@@ -156,7 +156,7 @@ const multiSeriesSpec: ChartSpec = {
};
// ---------------------------------------------------------------------------
-// 3. Five series with legend — when direct labels stop working
+// 3. Five series with explicit focus and readable comparison labels
// ---------------------------------------------------------------------------
const fiveSeriesSpec: ChartSpec = {
@@ -170,13 +170,19 @@ const fiveSeriesSpec: ChartSpec = {
type: 'quantitative',
axis: { title: 'Annual revenue ($B)', format: ',.0f', grid: true },
},
- color: { field: 'company', type: 'nominal' },
+ color: {
+ field: 'company',
+ type: 'nominal',
+ scale: {
+ domain: ['Amazon', 'Apple', 'Alphabet', 'Microsoft', 'Meta'],
+ range: ['#0e7490', '#7a858f', '#7a858f', '#7a858f', '#7a858f'],
+ },
+ },
},
- // legend.position implies show:true; endpointLabels:false hands series
- // identification to the legend alone (see the suppression truth table in
- // packages/core/src/types/spec.ts) so the two keys don't duplicate.
- legend: { position: 'top' },
- endpointLabels: false,
+ // Explicit focus mutes comparison strokes; direct labels keep every company identifiable.
+ legend: { show: false },
+ endpointLabels: { content: 'label', showMarker: true },
+ seriesStyles: { Amazon: { strokeWidth: 3 } },
labels: { density: 'none' },
chrome: {
title: 'Amazon Pulls Away at the Top of Big Tech',
@@ -235,8 +241,7 @@ const stackedAreaSpec: ChartSpec = {
y: {
field: 'energy',
type: 'quantitative',
- // Multi-series areas default to overlap; opt back into stacking with
- // `stack: 'zero'` on the position channel when the story is parts-of-a-whole.
+ // Explicit zero stacking makes the composition over time clear.
stack: 'zero',
axis: { title: 'Share of primary energy (%)', format: '.0f', grid: true },
},
@@ -465,7 +470,7 @@ export const LineAndArea = () => (
(
/>
@@ -500,12 +505,12 @@ export const LineAndArea = () => (
@@ -558,3 +563,48 @@ export const LineAndArea = () => (
);
+
+/** Opt-in prototype: boundary geometry and source values remain unchanged. */
+export const StackedBandLabels = () => (
+
+
+
+);
+
+/** Candidate spacing comparison only: the library default remains 6px. */
+export const EndpointSpacingCandidate = () => (
+
+ {[6, 8].map((endpointEntryGap) => (
+
+ [
+ { date: '2020-01-01', value: 5 + index * 3, series },
+ { date: '2021-01-01', value: 10 + index * 0.1, series },
+ ]),
+ encoding: {
+ x: { field: 'date', type: 'temporal' },
+ y: { field: 'value', type: 'quantitative', scale: { domain: [0, 20] } },
+ color: { field: 'series', type: 'nominal' },
+ },
+ endpointLabels: true,
+ animation: false,
+ theme: { spacing: { endpointEntryGap } },
+ chrome: {
+ subtitle: 'Synthetic fixture with close final values; typography stays unchanged.',
+ title: `Endpoint block separation: ${endpointEntryGap}px`,
+ },
+ }}
+ />
+
+ ))}
+
+);
diff --git a/examples/src/gallery/design-chart-candidates.stories.tsx b/examples/src/gallery/design-chart-candidates.stories.tsx
new file mode 100644
index 00000000..51c62135
--- /dev/null
+++ b/examples/src/gallery/design-chart-candidates.stories.tsx
@@ -0,0 +1,224 @@
+/** Review-only B3 plates. Query variant=before|after, surface=light|dark, text=2. */
+import type { ChartSpec, ThemeConfig } from '@opendata-ai/openchart-core';
+import { compileChart } from '@opendata-ai/openchart-engine';
+import { Chart } from '@opendata-ai/openchart-react';
+import { useLayoutEffect, useRef, useState } from 'react';
+import {
+ energyMix,
+ populationByCountry,
+ renewableCapacityAdditions,
+ temperatureAnomaly,
+ usPayrolls,
+} from '../data';
+
+export default { title: 'Design Chart Craft' };
+
+type Case = 'grid' | 'rows' | 'columns' | 'grouped' | 'endpoint' | 'band';
+
+function chartCraftSpec(kind: Case, candidate: boolean, _dark: boolean, textScale = 1): ChartSpec {
+ const theme: ThemeConfig = {
+ fonts: {
+ sizes: {
+ title: 26 * textScale,
+ subtitle: 14 * textScale,
+ small: 12 * textScale,
+ body: 13 * textScale,
+ axisTick: 12 * textScale,
+ endpointLabel: 13 * textScale,
+ endpointValue: 12 * textScale,
+ },
+ },
+ };
+ if (kind === 'grid')
+ theme.colors = {
+ gridline: {
+ light: candidate ? 'rgba(0,0,0,0.05)' : 'rgba(0,0,0,0.08)',
+ dark: 'rgba(255,255,255,0.06)',
+ },
+ gridlineZero: {
+ light: candidate ? 'rgba(0,0,0,0.22)' : 'rgba(0,0,0,0.08)',
+ dark: candidate ? 'rgba(255,255,255,0.24)' : 'rgba(255,255,255,0.06)',
+ },
+ };
+ const shared: Partial = { animation: false, theme };
+ if (kind === 'endpoint')
+ return {
+ ...shared,
+ mark: 'line',
+ data: ['A', 'B', 'C', 'D'].flatMap((series, index) => [
+ { date: '2020-01-01', value: 5 + index * 3, series },
+ { date: '2021-01-01', value: 10 + index * 0.1, series },
+ ]),
+ encoding: {
+ x: { field: 'date', type: 'temporal' },
+ y: { field: 'value', type: 'quantitative', scale: { domain: [0, 20] } },
+ color: { field: 'series', type: 'nominal' },
+ },
+ endpointLabels: true,
+ theme: { ...theme, spacing: { endpointEntryGap: candidate ? 8 : 6 } },
+ chrome: {
+ title: 'Close endpoints retain their identities',
+ subtitle: 'Existing synthetic gallery fixture; values and typography unchanged',
+ },
+ };
+ if (kind === 'band')
+ return {
+ ...shared,
+ mark: 'area',
+ data: [...energyMix.data],
+ encoding: {
+ x: { field: 'year', type: 'ordinal' },
+ y: {
+ field: 'energy',
+ type: 'quantitative',
+ stack: 'zero',
+ axis: { title: 'Share of primary energy (%)', format: '.0f' },
+ },
+ color: { field: 'source', type: 'nominal' },
+ },
+ endpointLabels: { anchor: candidate ? 'band' : 'boundary' },
+ legend: { show: false },
+ chrome: {
+ title: 'Fossil fuels still dominate the mix',
+ subtitle: 'Global primary energy mix, 2015–2022 (% of total)',
+ source: energyMix.source,
+ },
+ };
+ if (kind === 'rows')
+ return {
+ ...shared,
+ mark: 'bar',
+ data: [...populationByCountry.data],
+ encoding: {
+ x: {
+ field: 'population',
+ type: 'quantitative',
+ axis: { title: 'Population', format: '.2~s' },
+ },
+ y: {
+ field: 'country',
+ type: 'nominal',
+ sort: 'x',
+ scale: candidate ? { paddingInner: 3 / 7, paddingOuter: 3 / 14 } : undefined,
+ },
+ },
+ labels: { density: 'all', format: '.2~s', fontSize: 13 * textScale },
+ chrome: {
+ title: 'India leads the population ranking',
+ subtitle: 'Population by country, 2025 estimates',
+ source: populationByCountry.source,
+ },
+ };
+ if (kind === 'grouped')
+ return {
+ ...shared,
+ mark: 'bar',
+ data: [...renewableCapacityAdditions.data],
+ encoding: {
+ x: {
+ field: 'year',
+ type: 'nominal',
+ scale: candidate ? { paddingInner: 0.4, paddingOuter: 0.2 } : undefined,
+ },
+ y: {
+ field: 'capacity',
+ type: 'quantitative',
+ stack: null,
+ axis: { title: 'Capacity added (GW)' },
+ },
+ color: { field: 'type', type: 'nominal' },
+ },
+ labels: { density: 'none' },
+ chrome: {
+ title: 'Solar additions pull away from wind',
+ subtitle: 'Renewable capacity added, 2019–2023',
+ source: renewableCapacityAdditions.source,
+ },
+ };
+ if (kind === 'columns')
+ return {
+ ...shared,
+ mark: 'bar',
+ data: [...usPayrolls.data],
+ encoding: {
+ x: {
+ field: 'month',
+ type: 'nominal',
+ scale: candidate ? { paddingInner: 0.45, paddingOuter: 0.225 } : undefined,
+ },
+ y: { field: 'jobs', type: 'quantitative', axis: { title: 'Jobs added (thousands)' } },
+ },
+ labels: { density: 'auto', format: ',.0f', fontSize: 13 * textScale },
+ chrome: {
+ title: 'Hiring slowed during 2024',
+ subtitle: 'Monthly US nonfarm payroll additions',
+ source: usPayrolls.source,
+ },
+ };
+ return {
+ ...shared,
+ mark: 'bar',
+ data: [...temperatureAnomaly.data],
+ encoding: {
+ x: { field: 'year', type: 'ordinal' },
+ y: {
+ field: 'anomaly',
+ type: 'quantitative',
+ axis: { title: 'Temperature anomaly (°C)', format: '+.1f' },
+ },
+ color: { field: 'trend', type: 'nominal' },
+ },
+ labels: { density: 'none' },
+ chrome: {
+ title: 'Recent decades exceed the average',
+ subtitle: 'Global temperature relative to the 20th century',
+ source: temperatureAnomaly.source,
+ },
+ };
+}
+
+function Plate({ kind }: { kind: Case }) {
+ const params = new URLSearchParams(window.location.search);
+ const candidate = params.get('variant') === 'after';
+ const dark = params.get('surface') === 'dark';
+ const textScale = params.get('text') === '2' ? 2 : 1;
+ const host = useRef(null);
+ const [width, setWidth] = useState(1024);
+ useLayoutEffect(() => {
+ const node = host.current;
+ if (!node) return;
+ const observer = new ResizeObserver(([entry]) => setWidth(entry.contentRect.width));
+ observer.observe(node);
+ return () => observer.disconnect();
+ }, []);
+ const spec = chartCraftSpec(kind, candidate, dark, textScale);
+ let height = 480 * textScale;
+ if (kind === 'rows' && candidate) {
+ const measured = compileChart(spec, { width, height });
+ // Auto-height prototype: retain measured chrome/axis bands, change only
+ // the ten-row plot. With this padding denominator, step is exactly28px.
+ height = height - measured.area.height + populationByCountry.data.length * 28 * textScale;
+ }
+ return (
+
+
+
+ );
+}
+
+export const GridHierarchy = () => ;
+export const BarRowRhythm = () => ;
+export const ColumnRhythm = () => ;
+export const GroupedColumnRhythm = () => ;
+
+export const EndpointBlocks = () => ;
+export const StackedBandAnchors = () => ;
diff --git a/examples/src/gallery/features-annotations.stories.tsx b/examples/src/gallery/features-annotations.stories.tsx
index 590b5c36..2f0e2323 100644
--- a/examples/src/gallery/features-annotations.stories.tsx
+++ b/examples/src/gallery/features-annotations.stories.tsx
@@ -184,8 +184,7 @@ const richTextSpec: ChartSpec = {
},
};
-// The lede stack: set `subtitle` and the primary line takes bold automatically,
-// with the subtitle in muted regular below it. No fontWeight authoring.
+// Primary text and subtitle are regular by default; author emphasis explicitly.
const ledeSpec: ChartSpec = {
animation: true,
mark: 'line',
@@ -220,7 +219,7 @@ const ledeSpec: ChartSpec = {
],
chrome: {
title: 'A Lede, Then the Context',
- subtitle: 'Global surface temperature anomaly — subtitle promotes the first line to bold',
+ subtitle: 'Global surface temperature anomaly — a primary line with supporting context',
source: temperatureAnomaly.source,
byline: 'Chart: OpenChart',
},
@@ -328,9 +327,7 @@ const reflineSpec: ChartSpec = {
},
labels: { density: 'none' },
annotations: [
- // Horizontal target line, dashed. At the right edge the label sat on the
- // Nov/Dec bars; slide it into the Jun–Aug valley, where the line runs
- // clear above every bar top.
+ // Auto placement keeps the average label beside its rule as the chart resizes.
{
type: 'refline',
y: 216,
@@ -338,14 +335,9 @@ const reflineSpec: ChartSpec = {
style: 'dashed',
stroke: '#64748b',
strokeWidth: 1,
- labelAnchor: 'right',
- labelOffset: { dx: -245 },
+ labelPlacement: 'auto',
},
- // Horizontal threshold, dotted, styled as a soft floor. Every bar except
- // October crosses the 100K line, and October's gap has the storm-marker
- // line running through it, so there is no bar-free stretch at label height.
- // Park the label in the Jun–Aug valley above the bar tops instead; the red
- // text ties it to the red dotted line below it.
+ // A reserved right gutter keeps the low threshold label clear of the bars.
{
type: 'refline',
y: 100,
@@ -353,10 +345,9 @@ const reflineSpec: ChartSpec = {
style: 'dotted',
stroke: '#d1495b',
strokeWidth: 1,
- labelAnchor: 'left',
- labelOffset: { dx: 381, dy: -30 },
+ labelPlacement: 'outside',
},
- // Vertical marker with a raw strokeDash override (takes precedence over style).
+ // The event label uses the top lane; strokeDash takes precedence over style.
{
type: 'refline',
x: 'Oct',
@@ -364,7 +355,7 @@ const reflineSpec: ChartSpec = {
strokeDash: [2, 3],
stroke: '#94a3b8',
strokeWidth: 1,
- labelAnchor: 'top',
+ labelPlacement: 'outside',
},
],
chrome: {
@@ -629,7 +620,7 @@ export const Annotations = () => (
(
@@ -715,9 +706,10 @@ export function AnnotationGestures() {
let spec = structuredClone(annotationGestureSpec);
let revision = 0;
const chart = createChart(host.current, spec, {
- editable: true,
+ interaction: 'edit',
+ darkMode: 'auto',
revision,
- onEditTransaction: (tx) => {
+ onEdit: (tx) => {
const result = applyEditTransaction(spec, tx);
if (!result.ok) throw new Error(result.error.reason);
spec = result.spec;
diff --git a/examples/src/gallery/features-edit-mode.demos.ts b/examples/src/gallery/features-edit-mode.demos.ts
index 3389215a..8e4b0a2d 100644
--- a/examples/src/gallery/features-edit-mode.demos.ts
+++ b/examples/src/gallery/features-edit-mode.demos.ts
@@ -15,6 +15,6 @@ export const page: PageEntry = {
export: 'Edit Mode',
demos: [
{ id: 'editor', title: 'The editor' },
- { id: 'editable-toggle', title: 'The editable prop' },
+ { id: 'interaction-mode', title: 'The interaction prop' },
],
};
diff --git a/examples/src/gallery/features-edit-mode.stories.tsx b/examples/src/gallery/features-edit-mode.stories.tsx
index 0b11e06b..9d31db2a 100644
--- a/examples/src/gallery/features-edit-mode.stories.tsx
+++ b/examples/src/gallery/features-edit-mode.stories.tsx
@@ -1,3 +1,4 @@
+import { applyEditTransaction } from '@opendata-ai/openchart-core';
/**
* Features / Edit Mode — the interactive editing experience.
*
@@ -12,15 +13,8 @@
* Absorbs and replaces the old annotation-editing story.
*/
-import type {
- AnnotationOffset,
- ChartSpec,
- ElementEdit,
- ElementRef,
- RangeAnnotation,
- RefLineAnnotation,
-} from '@opendata-ai/openchart-core';
-import { Chart, type ChartHandle } from '@opendata-ai/openchart-react';
+import type { ChartSpec, EditTransaction, ElementRef } from '@opendata-ai/openchart-core';
+import { Chart, type ChartHandle, type InteractionMode } from '@opendata-ai/openchart-react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { GalleryPage, Section } from '../components';
import { segmentRevenue } from '../data';
@@ -255,7 +249,7 @@ function StudioStyles() {
// The editable spec surface (the parts a host app persists back)
// ---------------------------------------------------------------------------
-type EditableState = Pick;
+type EditableState = Pick;
function makePristineState(): EditableState {
return {
@@ -282,9 +276,7 @@ function makePristineState(): EditableState {
y: 71.42,
text: 'Holiday peak:\n$71.4B devices',
fontSize: 11,
- anchor: 'left',
connector: true,
- offset: { dx: -110, dy: -50 },
},
{
type: 'text',
@@ -292,9 +284,8 @@ function makePristineState(): EditableState {
y: 22.17,
text: 'Cloud overtakes\n2022 devices floor',
fontSize: 11,
- anchor: 'left',
+ anchor: 'top',
connector: true,
- offset: { dx: -30, dy: -34 },
},
],
chrome: {
@@ -323,7 +314,7 @@ function buildSpec(state: EditableState): ChartSpec {
},
color: { field: 'segment', type: 'nominal' },
},
- legend: { position: 'top' },
+ legend: state.legend ?? { position: 'top' },
annotations: state.annotations,
chrome: state.chrome,
};
@@ -334,7 +325,7 @@ function buildSpec(state: EditableState): ChartSpec {
// ---------------------------------------------------------------------------
type LogEntry = {
- callback: 'onSelect' | 'onDeselect' | 'onEdit' | 'onAnnotationEdit' | 'onTextEdit';
+ callback: 'onSelect' | 'onDeselect' | 'onEdit';
detail: string;
ts: number;
};
@@ -343,22 +334,14 @@ const CALLBACK_COLOR: Record = {
onSelect: '#06b6d4',
onDeselect: '#94a3b8',
onEdit: '#6366f1',
- onAnnotationEdit: '#8b5cf6',
- onTextEdit: '#ec4899',
};
-const oneLine = (s: string) => s.replace(/\n/g, ' ');
-const fmtOffset = (o?: AnnotationOffset) =>
- `{ dx: ${Math.round(o?.dx ?? 0)}, dy: ${Math.round(o?.dy ?? 0)} }`;
-
function describeRef(ref: ElementRef): string {
switch (ref.type) {
case 'annotation':
return `annotation[${ref.index}]`;
case 'chrome':
return `chrome.${ref.key}`;
- case 'series-label':
- return `series "${ref.series}"`;
case 'legend':
return 'legend';
case 'legend-entry':
@@ -458,130 +441,19 @@ function EditModeStudio() {
[pushLog],
);
- // Unified edit callback: covers connector drags, range/refline label drags,
- // chrome/series/legend moves, and text edits. We persist the ones that map to
- // our editable surface (annotations + chrome).
+ // Persist all authored operations atomically, including add/remove and legend edits.
const handleEdit = useCallback(
- (edit: ElementEdit) => {
- switch (edit.type) {
- case 'annotation':
- setState((prev) => ({
- ...prev,
- annotations: prev.annotations?.map((annotation, index) =>
- edit.element.type === 'annotation' &&
- index === edit.element.index &&
- annotation.type === 'text'
- ? {
- ...annotation,
- offset: edit.offset,
- ...(edit.anchor !== undefined ? { anchor: edit.anchor } : {}),
- ...(edit.responsive !== undefined ? { responsive: edit.responsive } : {}),
- }
- : annotation,
- ),
- }));
- pushLog(
- 'onEdit',
- `annotation[${'index' in edit.element ? edit.element.index : '?'}] "${oneLine(edit.annotation.text)}" → ${fmtOffset(edit.offset)}`,
- );
- break;
- case 'annotation-connector':
- setState((prev) => ({
- ...prev,
- annotations: prev.annotations?.map((a) => {
- if (a.type === 'text' && a.text === edit.annotation.text) {
- return {
- ...a,
- connectorOffset: { ...a.connectorOffset, [edit.endpoint]: edit.offset },
- };
- }
- return a;
- }),
- }));
- pushLog(
- 'onEdit',
- `connector[${'index' in edit.element ? edit.element.index : '?'}] ${edit.endpoint} → ${fmtOffset(edit.offset)}`,
- );
- break;
- case 'annotation-anchor':
- {
- const anchorIdx = edit.element.type === 'annotation' ? edit.element.index : -1;
- setState((prev) => ({
- ...prev,
- annotations: prev.annotations?.map((a, i) =>
- a.type === 'text' && i === anchorIdx ? { ...a, x: edit.x, y: edit.y } : a,
- ),
- }));
- pushLog('onEdit', `anchor → x: ${edit.x}, y: ${String(edit.y).slice(0, 6)}`);
- }
- break;
- case 'range-label':
- setState((prev) => ({
- ...prev,
- annotations: prev.annotations?.map((a) =>
- a.type === 'range' && (a as RangeAnnotation).label === edit.annotation.label
- ? { ...a, labelOffset: edit.labelOffset }
- : a,
- ),
- }));
- pushLog(
- 'onEdit',
- `range label[${'index' in edit.element ? edit.element.index : '?'}] → ${fmtOffset(edit.labelOffset)}`,
- );
- break;
- case 'refline-label':
- setState((prev) => ({
- ...prev,
- annotations: prev.annotations?.map((a) =>
- a.type === 'refline' && (a as RefLineAnnotation).label === edit.annotation.label
- ? { ...a, labelOffset: edit.labelOffset }
- : a,
- ),
- }));
- pushLog(
- 'onEdit',
- `refline label[${'index' in edit.element ? edit.element.index : '?'}] → ${fmtOffset(edit.labelOffset)}`,
- );
- break;
- case 'chrome':
- setState((prev) => ({
- ...prev,
- chrome: { ...prev.chrome, [edit.key]: { text: edit.text, offset: edit.offset } },
- }));
- pushLog('onEdit', `chrome.${edit.key} → ${fmtOffset(edit.offset)}`);
- break;
- case 'text-edit':
- setState((prev) => {
- const annotations = [...(prev.annotations ?? [])];
- const chrome = { ...prev.chrome };
- if (edit.element.type === 'annotation') {
- const ann = annotations[edit.element.index];
- if (ann?.type === 'text')
- annotations[edit.element.index] = { ...ann, text: edit.newText };
- else if (ann?.type === 'range')
- annotations[edit.element.index] = { ...ann, label: edit.newText };
- else if (ann?.type === 'refline')
- annotations[edit.element.index] = { ...ann, label: edit.newText };
- } else if (edit.element.type === 'chrome') {
- const cur = chrome[edit.element.key];
- chrome[edit.element.key] =
- cur && typeof cur === 'object' ? { ...cur, text: edit.newText } : edit.newText;
- }
- return { ...prev, annotations, chrome };
- });
- pushLog('onEdit', `text "${oneLine(edit.oldText)}" → "${oneLine(edit.newText)}"`);
- break;
- case 'add':
- setState((prev) => ({
- ...prev,
- annotations: [...(prev.annotations ?? []), edit.annotation],
- }));
- pushLog('onEdit', `add annotation "${oneLine(edit.annotation.text)}"`);
- break;
- default:
- pushLog('onEdit', edit.type);
- break;
- }
+ (transaction: EditTransaction) => {
+ setState((previous) => {
+ const result = applyEditTransaction(buildSpec(previous), transaction);
+ if (!result.ok) return previous;
+ return {
+ annotations: result.spec.annotations,
+ chrome: result.spec.chrome,
+ legend: result.spec.legend,
+ };
+ });
+ pushLog('onEdit', transaction.ops.map((op) => `${op.op} ${op.path}`).join(', '));
},
[pushLog],
);
@@ -619,7 +491,7 @@ function EditModeStudio() {
{/* Editor */}
-
+
Esc to deselect.
+ annotation. Use arrows to nudge, Shift+arrow for 10px, and Tab/Shift+Tab to select.
+ Press Esc to deselect.
@@ -735,8 +608,14 @@ const TOGGLE_CSS = `
}
`;
-function EditableToggleDemo() {
- const [editable, setEditable] = useState(false);
+const MODE_STATUS: Record
= {
+ read: 'No selection or editing; arrow keys move between data points',
+ select: 'Click or Tab selects; arrows still navigate data; nothing is edited',
+ edit: 'Select, drag, nudge, retitle, and delete; arrows nudge the selection',
+};
+
+function InteractionModeDemo() {
+ const [mode, setMode] = useState('read');
const [eventLog, setEventLog] = useState>([]);
const spec: ChartSpec = {
@@ -760,14 +639,12 @@ function EditableToggleDemo() {
y: 71.42,
text: 'Holiday peak',
fontSize: 11,
- anchor: 'left',
connector: true,
- offset: { dx: -90, dy: -40 },
},
],
chrome: {
- title: 'Editable Toggle Demo',
- subtitle: 'Toggle editable to enable/disable editing',
+ title: 'Interaction Mode Demo',
+ subtitle: 'Switch between read, select, and edit',
},
};
@@ -777,8 +654,16 @@ function EditableToggleDemo() {
);
}, []);
- const handleEdit = useCallback((edit: ElementEdit) => {
- setEventLog((prev) => [{ text: `onEdit: ${edit.type}`, ts: Date.now() }, ...prev].slice(0, 20));
+ const handleEdit = useCallback((edit: EditTransaction) => {
+ setEventLog((prev) =>
+ [
+ {
+ text: `onEdit: ${edit.ops.map((op) => `${op.op} ${op.path}`).join(', ')}`,
+ ts: Date.now(),
+ },
+ ...prev,
+ ].slice(0, 20),
+ );
}, []);
return (
@@ -786,27 +671,28 @@ function EditableToggleDemo() {
{/* biome-ignore lint/security/noDangerouslySetInnerHtml: static CSS constant, no user input */}
- setEditable((v) => !v)}
- >
- editable: {String(editable)}
-
-
- {editable ? 'Drag, select, and edit are active' : 'Edit interactions are suppressed'}
-
+ {(['read', 'select', 'edit'] as const).map((m) => (
+ setMode(m)}
+ >
+ interaction: '{m}'
+
+ ))}
+ {MODE_STATUS[mode]}
-
+
{eventLog.length === 0 ? (
- {editable
- ? 'Try selecting or dragging an element...'
- : 'Enable editable to interact with the chart.'}
+ {mode === 'read'
+ ? 'Switch to select or edit to interact with the chart.'
+ : 'Try selecting or dragging an element...'}
) : (
eventLog.map((e) =>
{e.text}
)
@@ -828,8 +714,8 @@ export const EditMode = () => (
lede={
<>
OpenChart charts are editable in place. Wire the edit callbacks and every drag, retitle, and
- reposition comes back as a typed event you fold into your spec — the same JSON you persist
- and re-render. Host apps drive selection through the controlled{' '}
+ reposition comes back as a transaction you apply atomically to your spec — the same JSON you
+ persist and re-render. Host apps drive selection through the controlled{' '}
selectedElement prop and the imperative{' '}
ChartHandle (
select /{' '}
@@ -846,11 +732,11 @@ export const EditMode = () => (
);
diff --git a/examples/src/gallery/features-theming.stories.tsx b/examples/src/gallery/features-theming.stories.tsx
index 769d1933..3e535459 100644
--- a/examples/src/gallery/features-theming.stories.tsx
+++ b/examples/src/gallery/features-theming.stories.tsx
@@ -15,7 +15,7 @@
*/
import type { ChartSpec, ThemeConfig } from '@opendata-ai/openchart-core';
-import { broadsheet, editorial, essay, terminal, wire } from '@opendata-ai/openchart-core';
+import { broadsheet, essay, terminal, wire } from '@opendata-ai/openchart-core';
import { Chart, VizThemeProvider } from '@opendata-ai/openchart-react';
import type { CSSProperties } from 'react';
import { useState } from 'react';
@@ -151,7 +151,7 @@ const compactBarSpec: ChartSpec = {
const PRESETS: { name: string; theme: ThemeConfig; note: string }[] = [
{
name: 'editorial',
- theme: editorial,
+ theme: {},
note: 'The default look. Clean sans, restrained neutrals.',
},
{ name: 'essay', theme: essay, note: 'Serif display titles, warm canvas, generous spacing.' },
@@ -514,7 +514,7 @@ export const Theming = () => (
diff --git a/examples/src/gallery/geo-labels.stories.tsx b/examples/src/gallery/geo-labels.stories.tsx
new file mode 100644
index 00000000..60c1a82a
--- /dev/null
+++ b/examples/src/gallery/geo-labels.stories.tsx
@@ -0,0 +1,323 @@
+import type { GeoMapSpec } from '@opendata-ai/openchart-core';
+import { useVizDarkMode, useVizTheme } from '@opendata-ai/openchart-react';
+import { createGeoMap, type GeoMapInstance } from '@opendata-ai/openchart-vanilla';
+import { useEffect, useRef } from 'react';
+import usStates from 'us-atlas/states-10m.json';
+
+export default { title: 'Gallery / Geographic labels' };
+
+const labelSpec: GeoMapSpec = {
+ type: 'map',
+ animation: false,
+ geo: { features: usStates, projection: 'albersUsa', zoom: true },
+ data: [
+ { id: '06', place: 'California coast', value: 39, priority: 8 },
+ { id: '48', place: 'Texas triangle', value: 31, priority: 10 },
+ { id: '36', place: 'New York', value: 20, priority: 7 },
+ { id: '53', place: 'Pacific Northwest', value: 8, priority: 6 },
+ ],
+ encoding: {
+ key: { field: 'id', type: 'nominal' },
+ color: { field: 'value', type: 'quantitative', title: 'Population (millions)' },
+ text: { field: 'place', type: 'nominal' },
+ textPriority: { field: 'priority', type: 'quantitative' },
+ },
+ points: {
+ data: [
+ { id: 'austin', name: 'Austin', lon: -97.7431, lat: 30.2672, value: 2.5 },
+ { id: 'houston', name: 'Houston', lon: -95.3698, lat: 29.7604, value: 7.5 },
+ ],
+ longitude: { field: 'lon', type: 'quantitative' },
+ latitude: { field: 'lat', type: 'quantitative' },
+ key: { field: 'id', type: 'nominal' },
+ text: { field: 'name', type: 'nominal' },
+ size: { field: 'value', type: 'quantitative', title: 'Metro population' },
+ },
+ callouts: [
+ {
+ id: 'texas-growth',
+ anchor: { kind: 'feature', id: '48' },
+ text: 'The Texas triangle',
+ subtitle: 'An authored explanation stays available when its label cannot fit.',
+ priority: 10,
+ maxWidth: 200,
+ },
+ {
+ id: 'coordinate-place',
+ anchor: { kind: 'coordinate', space: 'geographic', x: -104.99, y: 39.74 },
+ text: 'A place without a data row',
+ maxWidth: 160,
+ },
+ {
+ id: 'austin-detail',
+ anchor: { kind: 'point', id: 'austin' },
+ text: 'Austin detail at a closer zoom',
+ minZoom: 3,
+ maxWidth: 180,
+ },
+ ],
+ chrome: {
+ title: 'Places stay attached as the view changes',
+ subtitle: 'Explicit place names and authored callouts; values are illustrative.',
+ source: 'Geometry: US Census Bureau. Illustrative observations.',
+ },
+};
+
+function LabelMap({
+ secondary = false,
+ largeText = false,
+}: {
+ secondary?: boolean;
+ largeText?: boolean;
+}) {
+ const theme = useVizTheme();
+ const darkMode = useVizDarkMode();
+ const hostRef = useRef(null);
+ const inventoryRef = useRef(null);
+ const alternateRef = useRef(null);
+ const instanceRef = useRef(null);
+ useEffect(() => {
+ const host = hostRef.current!;
+ const instance = createGeoMap(host, labelSpec, {
+ labelInventory: secondary ? false : { container: inventoryRef.current! },
+ darkMode,
+ theme: largeText
+ ? {
+ ...theme,
+ fonts: { ...theme?.fonts, sizes: { title: 52, subtitle: 28, body: 26, small: 24 } },
+ }
+ : theme,
+ onCameraChange: (camera) => {
+ host.dataset.camera = JSON.stringify(camera);
+ },
+ });
+ instanceRef.current = instance;
+ host.dataset.camera = JSON.stringify(instance.getCamera());
+ const exportSVG = () => {
+ host.dataset.export = String(instance.export('svg'));
+ };
+ const setCamera = (event: Event) => {
+ instance.setCamera((event as CustomEvent>).detail);
+ };
+ // Rejection is an automated fixture, not a public gallery action.
+ const rejectInvalidUpdate = () => {
+ try {
+ instance.update({
+ ...labelSpec,
+ callouts: [labelSpec.callouts![0], labelSpec.callouts![0]],
+ });
+ } catch (error) {
+ host.dataset.validationError = String(error);
+ }
+ };
+ host.addEventListener('fixture-invalid-update', rejectInvalidUpdate);
+ host.addEventListener('fixture-export', exportSVG);
+ host.addEventListener('fixture-camera', setCamera);
+ return () => {
+ host.removeEventListener('fixture-invalid-update', rejectInvalidUpdate);
+ host.removeEventListener('fixture-export', exportSVG);
+ host.removeEventListener('fixture-camera', setCamera);
+ instance.destroy();
+ instanceRef.current = null;
+ };
+ }, [secondary, largeText, theme, darkMode]);
+
+ return (
+
+ {!secondary && (
+
+ instanceRef.current?.zoomTo('48', { duration: 0 })}>
+ Show Texas
+
+ instanceRef.current?.resetView({ duration: 0 })}>
+ Full map
+
+
+ instanceRef.current?.update({
+ ...labelSpec,
+ data: labelSpec.data.map((row) =>
+ row.id === '48' ? { ...row, place: 'Texas updated' } : row,
+ ),
+ })
+ }
+ >
+ Update a place name
+
+
+ instanceRef.current?.setLabelInventory({ container: alternateRef.current! })
+ }
+ >
+ Move place list
+
+
+ )}
+
+
+
+
+ );
+}
+
+export const PlacesAndNotes = () => (
+
+
+
+);
+
+export const LargeText = () => (
+
+
+
+);
+
+export const MultipleMaps = () => (
+
+
+
+
+);
+
+const islandTopology = {
+ type: 'Topology',
+ objects: {
+ places: {
+ type: 'GeometryCollection',
+ geometries: [
+ { type: 'Polygon', id: 'ring', properties: { place: 'Around the lake' }, arcs: [[0], [1]] },
+ {
+ type: 'MultiPolygon',
+ id: 'islands',
+ properties: { place: 'Island group' },
+ arcs: [[[2]], [[3]]],
+ },
+ { type: 'Polygon', id: 'coast', properties: { place: 'The sheltered coast' }, arcs: [[4]] },
+ ],
+ },
+ },
+ arcs: [
+ [
+ [0, 0],
+ [0, 100],
+ [100, 100],
+ [100, 0],
+ [0, 0],
+ ],
+ [
+ [30, 30],
+ [70, 30],
+ [70, 70],
+ [30, 70],
+ [30, 30],
+ ],
+ [
+ [120, 0],
+ [120, 100],
+ [220, 100],
+ [220, 0],
+ [120, 0],
+ ],
+ [
+ [260, 10],
+ [260, 50],
+ [300, 50],
+ [300, 10],
+ [260, 10],
+ ],
+ [
+ [0, 130],
+ [100, 130],
+ [100, 230],
+ [65, 230],
+ [65, 165],
+ [35, 165],
+ [35, 230],
+ [0, 230],
+ [0, 130],
+ ],
+ ],
+};
+
+export const IslandsAndHoles = () => {
+ const theme = useVizTheme();
+ const darkMode = useVizDarkMode();
+ const hostRef = useRef(null);
+ const listRef = useRef(null);
+ const instanceRef = useRef(null);
+ useEffect(() => {
+ const instance = createGeoMap(
+ hostRef.current!,
+ {
+ type: 'map',
+ animation: false,
+ geo: { features: islandTopology, projection: 'identity', zoom: true },
+ data: [{ id: 'ring', value: 10 }],
+ encoding: {
+ key: { field: 'id', type: 'nominal' },
+ color: { field: 'value', type: 'quantitative' },
+ text: { field: 'place', type: 'nominal' },
+ },
+ callouts: [
+ {
+ id: 'unresolved',
+ anchor: { kind: 'feature', id: 'not-in-this-map' },
+ text: 'A place outside this supplied geography',
+ },
+ {
+ id: 'source-place',
+ anchor: { kind: 'coordinate', space: 'source', x: 145, y: 75 },
+ text: 'An authored position',
+ maxWidth: 130,
+ },
+ ],
+ chrome: {
+ title: 'Labels find the visible land',
+ subtitle:
+ 'Schematic islands, a lake and a concave coast. Names remain accessible when hidden.',
+ },
+ },
+ { theme, darkMode, labelInventory: { container: listRef.current! } },
+ );
+ instanceRef.current = instance;
+ return () => {
+ instance.destroy();
+ instanceRef.current = null;
+ };
+ }, [theme, darkMode]);
+ const showSmallIsland = () => {
+ const instance = instanceRef.current;
+ const components = instance?.layout.labels?.geometries.find(
+ (g) => g.featureId === 'islands',
+ )?.components;
+ if (!instance || !components?.length) return;
+ const component = [...components].sort(
+ (a, b) => Math.min(...b.exterior.map((p) => p.x)) - Math.min(...a.exterior.map((p) => p.x)),
+ )[0];
+ const x = Math.min(...component.exterior.map((p) => p.x));
+ const y = Math.min(...component.exterior.map((p) => p.y));
+ const width = Math.max(...component.exterior.map((p) => p.x)) - x;
+ const height = Math.max(...component.exterior.map((p) => p.y)) - y;
+ instance.fitBounds({ x, y, width, height, padding: 12 }, { duration: 0 });
+ };
+ return (
+
+
+
+ Show the small island
+
+ instanceRef.current?.resetView({ duration: 0 })}>
+ All islands
+
+
+
+
+
+ );
+};
diff --git a/examples/src/gallery/geo-views.stories.tsx b/examples/src/gallery/geo-views.stories.tsx
new file mode 100644
index 00000000..ddd1c922
--- /dev/null
+++ b/examples/src/gallery/geo-views.stories.tsx
@@ -0,0 +1,355 @@
+import type { GeoMapSpec } from '@opendata-ai/openchart-core';
+import { useVizDarkMode, useVizTheme } from '@opendata-ai/openchart-react';
+import {
+ createGeoMap,
+ createGeoReader,
+ type GeoMapInstance,
+ type GeoReaderController,
+} from '@opendata-ai/openchart-vanilla';
+import { geoArea } from 'd3-geo';
+import type { FeatureCollection, Geometry } from 'geojson';
+import { useEffect, useRef, useState } from 'react';
+import usCounties from 'us-atlas/counties-10m.json';
+import usStates from 'us-atlas/states-10m.json';
+import tractSource from '../../../packages/engine/src/geo/__tests__/fixtures/travis-tracts-2020.geojson?raw';
+
+export default { title: 'Gallery / Geographic views' };
+
+// Census GeoJSON follows RFC 7946 winding; D3 spherical paths use its reverse.
+const tracts = JSON.parse(tractSource) as FeatureCollection;
+for (const feature of tracts.features) {
+ if (geoArea(feature) <= Math.PI * 2) continue;
+ const geometry = feature.geometry as Geometry;
+ if (geometry.type === 'Polygon')
+ geometry.coordinates = geometry.coordinates.map((ring) => [...ring].reverse());
+ else if (geometry.type === 'MultiPolygon')
+ geometry.coordinates = geometry.coordinates.map((polygon) =>
+ polygon.map((ring) => [...ring].reverse()),
+ );
+}
+const texasTopology = {
+ ...usCounties,
+ objects: {
+ counties: {
+ ...usCounties.objects.counties,
+ geometries: usCounties.objects.counties.geometries.filter((feature) =>
+ String(feature.id).startsWith('48'),
+ ),
+ },
+ },
+};
+const texasDetailSpec: GeoMapSpec = {
+ type: 'map',
+ animation: false,
+ geo: {
+ features: texasTopology,
+ objectKey: 'counties',
+ fitObjectKey: 'counties',
+ projection: 'albersUsa',
+ zoom: true,
+ detailLayers: [{ id: 'travis-tracts', features: tracts, minZoom: 4 }],
+ },
+ data: [],
+ encoding: {},
+ points: {
+ data: [
+ { id: 'austin', name: 'Austin', longitude: -97.7431, latitude: 30.2672 },
+ { id: 'mcallen', name: 'McAllen', longitude: -98.23, latitude: 26.2034 },
+ { id: 'brownsville', name: 'Brownsville', longitude: -97.4975, latitude: 25.9017 },
+ { id: 'san-antonio', name: 'San Antonio', longitude: -98.4936, latitude: 29.4241 },
+ ],
+ key: { field: 'id', type: 'nominal' },
+ longitude: { field: 'longitude', type: 'quantitative' },
+ latitude: { field: 'latitude', type: 'quantitative' },
+ text: { field: 'name', type: 'nominal' },
+ tooltip: [{ field: 'name', type: 'nominal', title: 'City' }],
+ },
+ chrome: {
+ title: 'Texas context, Austin detail',
+ subtitle: 'County boundaries stay fixed. Travis County census tracts appear at 4× zoom.',
+ source:
+ 'US Atlas counties; Census Bureau TIGERweb Census2020 tracts, simplified 0.002°. City coordinates are illustrative.',
+ },
+};
+const usInsetSpec: GeoMapSpec = {
+ type: 'map',
+ animation: false,
+ geo: {
+ features: usStates,
+ objectKey: 'states',
+ projection: 'albersUsa',
+ zoom: true,
+ insets: [
+ {
+ id: 'alaska',
+ label: 'Alaska',
+ featureIds: ['02'],
+ pointIds: ['anchorage'],
+ placement: 'bottom-left',
+ width: 140,
+ },
+ {
+ id: 'hawaii',
+ label: 'Hawaii',
+ featureIds: ['15'],
+ pointIds: ['honolulu'],
+ placement: 'bottom-right',
+ width: 140,
+ },
+ ],
+ },
+ data: [],
+ encoding: {},
+ points: {
+ data: [
+ { id: 'anchorage', name: 'Anchorage', longitude: -149.9003, latitude: 61.2181 },
+ { id: 'honolulu', name: 'Honolulu', longitude: -157.8583, latitude: 21.3069 },
+ ],
+ key: { field: 'id', type: 'nominal' },
+ longitude: { field: 'longitude', type: 'quantitative' },
+ latitude: { field: 'latitude', type: 'quantitative' },
+ text: { field: 'name', type: 'nominal' },
+ },
+ chrome: {
+ title: 'Separate views, shared places',
+ subtitle:
+ 'Alaska and Hawaii have independent scales and cameras. Their features move explicitly out of the main view.',
+ source: 'US Atlas state boundaries. City coordinates are illustrative.',
+ },
+};
+
+function ViewMap({
+ spec,
+ testId,
+ reader = false,
+ largeText = false,
+}: {
+ spec: GeoMapSpec;
+ testId: string;
+ reader?: boolean;
+ largeText?: boolean;
+}) {
+ const host = useRef(null);
+ const [largerCaptions, setLargerCaptions] = useState(largeText);
+ const instanceRef = useRef(null);
+ const readerRef = useRef(null);
+ const panelRef = useRef(null);
+ const detailEnabled = useRef(true);
+ const theme = useVizTheme(),
+ darkMode = useVizDarkMode();
+ useEffect(() => {
+ const node = host.current!;
+ const snapshot = () => {
+ const instance = instanceRef.current;
+ if (!instance) return;
+ node.dataset.cameras = JSON.stringify(instance.getCameras());
+ node.dataset.points = JSON.stringify(
+ instance.layout.pointMarks.map(({ key, cx, cy, r }) => ({ key, cx, cy, r })),
+ );
+ node.dataset.views = JSON.stringify(
+ instance.layout.views?.map((view) => ({
+ id: view.viewId,
+ features: view.features.map((feature) => feature.id),
+ points: view.pointMarks.map((point) => point.key),
+ })) ?? [],
+ );
+ };
+ const instance = createGeoMap(node, spec, {
+ theme: largerCaptions
+ ? { ...theme, fonts: { ...theme?.fonts, sizes: { ...theme?.fonts?.sizes, small: 24 } } }
+ : theme,
+ darkMode,
+ onCameraChange: snapshot,
+ onMarkHover: (event) => {
+ node.dataset.markEvent = JSON.stringify(event);
+ },
+ });
+ instanceRef.current = instance;
+ if (reader)
+ readerRef.current = createGeoReader(
+ node,
+ instance,
+ spec,
+ (selection) => {
+ node.dataset.reader = JSON.stringify(selection);
+ },
+ { panel: { container: panelRef.current! } },
+ );
+ snapshot();
+ const exportSVG = () => {
+ node.dataset.export = String(instance.export('svg'));
+ };
+ const camera = (event: Event) => {
+ const detail = (
+ event as CustomEvent<{ viewId?: string; camera: ReturnType }>
+ ).detail;
+ instance.setCamera(detail.camera, { viewId: detail.viewId });
+ snapshot();
+ };
+ const rejectPlane = () => {
+ try {
+ instance.update({
+ ...spec,
+ geo: {
+ ...spec.geo,
+ projection: 'identity',
+ sourcePlane: 'primary-plane',
+ detailLayers: [
+ {
+ id: 'invalid-plane',
+ features: texasTopology,
+ minZoom: 2,
+ sourcePlane: 'other-plane',
+ },
+ ],
+ },
+ });
+ } catch (error) {
+ node.dataset.validationError = String(error);
+ }
+ snapshot();
+ };
+ const rejectScope = (event: Event) => {
+ const scope = (event as CustomEvent<'view' | 'layer'>).detail;
+ delete node.dataset.scopeError;
+ try {
+ instance.update({
+ ...spec,
+ callouts: [
+ {
+ id: 'invalid-scope',
+ text: 'Invalid scope',
+ anchor: {
+ kind: 'feature',
+ id: '48453',
+ ...(scope === 'view' ? { viewId: 'missing-view' } : { layerId: 'missing-layer' }),
+ },
+ },
+ ],
+ });
+ } catch (error) {
+ node.dataset.scopeError = String(error);
+ }
+ snapshot();
+ };
+ node.addEventListener('fixture-invalid-scope', rejectScope);
+ node.addEventListener('fixture-export', exportSVG);
+ node.addEventListener('fixture-camera', camera);
+ node.addEventListener('fixture-invalid-plane', rejectPlane);
+ return () => {
+ node.removeEventListener('fixture-invalid-scope', rejectScope);
+ node.removeEventListener('fixture-export', exportSVG);
+ node.removeEventListener('fixture-camera', camera);
+ node.removeEventListener('fixture-invalid-plane', rejectPlane);
+ readerRef.current?.destroy();
+ readerRef.current = null;
+ instance.destroy();
+ instanceRef.current = null;
+ };
+ }, [spec, theme, darkMode, reader, largerCaptions]);
+ return (
+
+
+ {reader && (
+ readerRef.current?.pin({ kind: 'feature', id: '02', canonicalId: '02' })}
+ >
+ Pin Alaska
+
+ )}
+ {spec.geo.detailLayers && (
+ <>
+ instanceRef.current?.zoomTo('48453', { duration: 0 })}
+ >
+ Austin detail
+
+ {
+ detailEnabled.current = !detailEnabled.current;
+ instanceRef.current?.update({
+ ...spec,
+ geo: {
+ ...spec.geo,
+ detailLayers: detailEnabled.current ? spec.geo.detailLayers : [],
+ },
+ });
+ const instance = instanceRef.current;
+ if (instance)
+ host.current!.dataset.points = JSON.stringify(
+ instance.layout.pointMarks.map(({ key, cx, cy, r }) => ({ key, cx, cy, r })),
+ );
+ }}
+ >
+ Toggle tract context
+
+ >
+ )}
+ {spec.geo.insets && (
+ <>
+ setLargerCaptions((value) => !value)}>
+ {largerCaptions ? 'Default caption text' : 'Enlarge caption text'}
+
+
+ instanceRef.current?.zoomTo('02', { viewId: 'alaska', duration: 0, padding: 8 })
+ }
+ >
+ Alaska view
+
+
+ instanceRef.current?.zoomTo('15', { viewId: 'hawaii', duration: 0, padding: 8 })
+ }
+ >
+ Hawaii view
+
+ >
+ )}
+ instanceRef.current?.resetView({ viewId: 'root', duration: 0 })}
+ >
+ Reset main view
+
+
+
+ {reader &&
}
+
+ );
+}
+export function TexasAndAustin() {
+ return ;
+}
+export function SeparateScales() {
+ return ;
+}
+export function TwoMaps() {
+ return (
+ <>
+
+
+ >
+ );
+}
+
+const duplicatedPlaceSpec: GeoMapSpec = {
+ ...usInsetSpec,
+ geo: {
+ ...usInsetSpec.geo,
+ insets: usInsetSpec.geo.insets!.map((inset) => ({ ...inset, removeFromMain: false })),
+ },
+ chrome: {
+ ...usInsetSpec.chrome,
+ subtitle:
+ 'The same Alaska observation appears in the main map and a separate-scale inset. Pin it once to highlight both.',
+ },
+};
+export function DuplicatedPlace() {
+ return ;
+}
diff --git a/examples/src/gallery/maps.stories.tsx b/examples/src/gallery/maps.stories.tsx
index 80b1717d..a6fa6431 100644
--- a/examples/src/gallery/maps.stories.tsx
+++ b/examples/src/gallery/maps.stories.tsx
@@ -9,7 +9,7 @@
import type { GeoMapSpec } from '@opendata-ai/openchart-core';
import type { GeoMapHandle } from '@opendata-ai/openchart-react';
import { ChartStory, GeoMap } from '@opendata-ai/openchart-react';
-import { useRef, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
import usCountiesTopo from 'us-atlas/counties-albers-10m.json';
import usStatesUnprojected from 'us-atlas/states-10m.json';
import usStatesTopo from 'us-atlas/states-albers-10m.json';
@@ -600,3 +600,141 @@ export const Maps = () => (
);
+
+/** Explicit P2 compositions: narrow frames demonstrate the diagnosed top fallback. */
+export const SideLegendComposition = () => (
+
+
+
+);
+
+/** Isolated renderer experiment; no production pattern default or spec API changes. */
+function CraftMapPrototype({ spec, candidate }: { spec: GeoMapSpec; candidate: boolean }) {
+ const host = useRef
(null);
+ useEffect(() => {
+ const root = host.current;
+ if (!root || !candidate) return;
+ const apply = () => {
+ for (const pattern of root.querySelectorAll('pattern')) {
+ pattern.setAttribute('width', '10');
+ pattern.setAttribute('height', '10');
+ const base = pattern.querySelector('rect');
+ base?.setAttribute('width', '10');
+ base?.setAttribute('height', '10');
+ const hatch = pattern.querySelector('path');
+ hatch?.setAttribute('d', 'M1,3 l2,-2 M6,8 l2,-2');
+ hatch?.setAttribute('stroke-width', '0.9');
+ }
+ };
+ apply();
+ const observer = new MutationObserver(apply);
+ observer.observe(root, { childList: true, subtree: true });
+ return () => observer.disconnect();
+ }, [candidate]);
+ return (
+
+
+
+ );
+}
+
+/** G8 evidence candidate; the toggle changes only explicit authored theme colors. */
+export const MapCraftCandidates = () => {
+ const [candidate, setCandidate] = useState(false);
+ const theme = candidate
+ ? {
+ colors: {
+ mapNoDataFill: { light: '#f5f5f5', dark: '#252525' },
+ mapNoDataHatch: { light: '#707070', dark: '#aaaaaa' },
+ mapInteriorBorder: { light: 'rgba(0,0,0,0.14)', dark: 'rgba(255,255,255,0.18)' },
+ mapOutlineBorder: { light: 'rgba(0,0,0,0.40)', dark: 'rgba(255,255,255,0.38)' },
+ },
+ }
+ : undefined;
+ const fixtures: Array<{ id: string; title: string; spec: GeoMapSpec }> = [
+ { id: 'craft-world', title: 'Sparse world coverage', spec: worldEqualEarthSpec },
+ {
+ id: 'craft-county',
+ title: 'County values and missingness (synthetic)',
+ spec: {
+ ...usCountySpec,
+ data: (
+ usCountiesTopo as {
+ objects: { counties: { geometries: Array<{ id: string | number }> } };
+ }
+ ).objects.counties.geometries
+ .filter((_, index) => index % 4 !== 0)
+ .map((feature, index) => ({ id: feature.id, rate: [-10, -5, 0, 5, 10][index % 5] })),
+ chrome: {
+ title: 'Observed Zero and Missing Counties Are Different',
+ subtitle:
+ 'Synthetic illustrative values: 75% observed counties, 25% missing; no geographic exclusions',
+ },
+ encoding: {
+ ...usCountySpec.encoding,
+ color: {
+ field: 'rate',
+ type: 'quantitative',
+ scale: { scheme: 'redBlue', domain: [-10, 10] },
+ },
+ },
+ },
+ },
+ { id: 'craft-bubbles', title: 'Bubble map context', spec: pointLayerSpec },
+ ];
+ return (
+
+ setCandidate(!candidate)}>
+ {candidate ? 'Show current defaults' : 'Show candidate colors'}
+
+
+ {fixtures.map(({ id, title, spec }) => (
+
+
+
+ ))}
+
+
+ );
+};
diff --git a/examples/src/gallery/playground.stories.tsx b/examples/src/gallery/playground.stories.tsx
index b68f778b..37b80b35 100644
--- a/examples/src/gallery/playground.stories.tsx
+++ b/examples/src/gallery/playground.stories.tsx
@@ -16,7 +16,7 @@
import type { Story } from '@ladle/react';
import type { ChartSpec, ThemeConfig } from '@opendata-ai/openchart-core';
-import { editorial, essay, wire } from '@opendata-ai/openchart-core';
+import { essay, wire } from '@opendata-ai/openchart-core';
import { Demo, GalleryPage } from '../components';
import { bigTechRevenue } from '../data';
@@ -54,7 +54,7 @@ const DONUT_DATA = SERIES.filter((d) => d.year === '2024-01-01').map((d) => ({
revenue: d.revenue,
}));
-const THEMES: Record = { editorial, essay, wire };
+const THEMES: Record = { editorial: {}, essay, wire };
/** Presets that support stacking (parts-of-a-whole marks). */
const STACKABLE: ReadonlySet = new Set(['bar', 'column', 'area']);
diff --git a/examples/src/gallery/reader.stories.tsx b/examples/src/gallery/reader.stories.tsx
new file mode 100644
index 00000000..424116f8
--- /dev/null
+++ b/examples/src/gallery/reader.stories.tsx
@@ -0,0 +1,212 @@
+import type { ChartSpec, GeoMapSpec } from '@opendata-ai/openchart-core';
+import { useVizDarkMode, useVizTheme } from '@opendata-ai/openchart-react';
+import {
+ createChart,
+ createGeoMap,
+ createGeoReader,
+ createReader,
+} from '@opendata-ai/openchart-vanilla';
+import { useEffect, useRef } from 'react';
+import usStates from 'us-atlas/states-10m.json';
+
+export default { title: 'Gallery / Reader comparison' };
+const line: ChartSpec = {
+ mark: 'line',
+ animation: false,
+ data: [
+ { id: 'n2022', year: '2022', rate: 12, region: 'North' },
+ { id: 'n2023', year: '2023', rate: 15, region: 'North' },
+ { id: 'n2024', year: '2024', rate: 18, region: 'North' },
+ { id: 's2022', year: '2022', rate: 8, region: 'South' },
+ { id: 's2024', year: '2024', rate: 11, region: 'South' },
+ ],
+ encoding: {
+ x: { field: 'year', type: 'ordinal', axis: { title: '' } },
+ y: { field: 'rate', type: 'quantitative', title: 'Rate (%)' },
+ color: { field: 'region', type: 'nominal' },
+ key: { field: 'id', type: 'nominal' },
+ },
+ fields: { rate: { measure: 'participation', representation: 'percent', unit: '%' } },
+ chrome: {
+ title: 'Compare two years',
+ subtitle: 'Select a date to pin it. Arrow keys inspect; Enter pins.',
+ source: 'Illustrative observations. No South observation was recorded in 2023.',
+ },
+};
+export function Dates() {
+ const host = useRef(null),
+ panel = useRef(null),
+ alternate = useRef(null);
+ const actions = useRef<{ reorder(): void; remove(): void; move(): void } | null>(null);
+ const theme = useVizTheme(),
+ darkMode = useVizDarkMode();
+ useEffect(() => {
+ let spec = line;
+ const chart = createChart(host.current!, spec, { theme, darkMode });
+ const options = {
+ mode: 'slices' as const,
+ panel: { container: panel.current! },
+ comparison: 'absolute' as const,
+ };
+ const reader = createReader(
+ host.current!,
+ chart,
+ spec,
+ (selection, meta) => {
+ host.current!.dataset.reader = JSON.stringify(selection);
+ host.current!.dataset.reason = meta.reason;
+ },
+ options,
+ );
+ actions.current = {
+ reorder() {
+ spec = { ...spec, data: [...(line.data as Record[])].reverse() };
+ chart.update(spec);
+ },
+ remove() {
+ spec = {
+ ...spec,
+ data: (line.data as Record[]).filter((row) => row.year !== '2024'),
+ };
+ chart.update(spec);
+ },
+ move() {
+ reader.setOptions({ ...options, panel: { container: alternate.current! } });
+ },
+ };
+ return () => {
+ actions.current = null;
+ reader.destroy();
+ chart.destroy();
+ };
+ }, [theme, darkMode]);
+ return (
+
+
+
+
+ actions.current?.reorder()}
+ >
+ Reorder observations
+ {' '}
+ actions.current?.remove()}
+ >
+ Remove 2024
+ {' '}
+ actions.current?.move()}
+ >
+ Move comparison panel
+
+
+
+
+ );
+}
+const map: GeoMapSpec = {
+ type: 'map',
+ animation: false,
+ geo: { features: usStates, projection: 'albersUsa', zoom: true },
+ data: [
+ { id: '06', place: 'California', value: 42 },
+ { id: '48', place: 'Texas', value: 31 },
+ { id: '36', place: 'New York', value: 17 },
+ { id: '53', place: 'Washington', value: 0 },
+ ],
+ encoding: {
+ key: { field: 'id', type: 'nominal' },
+ color: { field: 'value', type: 'quantitative', title: 'Participation (%)' },
+ text: { field: 'place', type: 'nominal' },
+ },
+ fields: { value: { measure: 'participation', representation: 'percent', unit: '%' } },
+ colorKey: { activeValue: true, distribution: true },
+ chrome: {
+ title: 'Compare two places',
+ subtitle: 'Find and pin two states. Show on map is a separate action.',
+ source: 'Illustrative observations; unjoined states have no data.',
+ },
+};
+export function Places() {
+ const host = useRef(null),
+ panel = useRef(null),
+ companion = useRef(null);
+ const theme = useVizTheme(),
+ darkMode = useVizDarkMode();
+ useEffect(() => {
+ const element = host.current!;
+ const instance = createGeoMap(element, map, {
+ theme,
+ darkMode,
+ onCameraChange(camera) {
+ element.dataset.camera = JSON.stringify(camera);
+ },
+ });
+ element.dataset.camera = JSON.stringify(instance.getCamera());
+ const exportSVG = () => {
+ element.dataset.export = String(instance.export('svg'));
+ };
+ element.addEventListener('fixture-export', exportSVG);
+ const reader = createGeoReader(
+ host.current!,
+ instance,
+ map,
+ (selection, meta) => {
+ host.current!.dataset.reader = JSON.stringify(selection);
+ host.current!.dataset.reason = meta.reason;
+ },
+ {
+ panel: { container: panel.current! },
+ companion: { container: companion.current!, rank: 'descending', pageSize: 8 },
+ comparison: 'absolute',
+ },
+ );
+ return () => {
+ element.removeEventListener('fixture-export', exportSVG);
+ reader.destroy();
+ instance.destroy();
+ };
+ }, [theme, darkMode]);
+ return (
+
+
+
+
+
+ );
+}
diff --git a/examples/src/gallery/row-hover.stories.tsx b/examples/src/gallery/row-hover.stories.tsx
new file mode 100644
index 00000000..27a07aea
--- /dev/null
+++ b/examples/src/gallery/row-hover.stories.tsx
@@ -0,0 +1,28 @@
+import type { ChartSpec } from '@opendata-ai/openchart-core';
+import { Chart } from '@opendata-ai/openchart-react';
+
+export default { title: 'Gallery / Row inspection' };
+const spec: ChartSpec = {
+ mark: 'bar',
+ animation: false,
+ chrome: {
+ title: 'Compare a row',
+ subtitle: 'Move across a category or value, or use arrow keys.',
+ },
+ data: [
+ { name: 'North', value: 82 },
+ { name: 'South', value: 61 },
+ { name: 'East', value: 49 },
+ { name: 'West', value: 33 },
+ ],
+ encoding: {
+ y: { field: 'name', type: 'nominal', sort: null },
+ x: { field: 'value', type: 'quantitative' },
+ },
+ labels: { placement: 'column' },
+};
+export const RowHover = () => (
+
+
+
+);
diff --git a/examples/src/gallery/showcase.stories.tsx b/examples/src/gallery/showcase.stories.tsx
index 47c7db40..d93710ea 100644
--- a/examples/src/gallery/showcase.stories.tsx
+++ b/examples/src/gallery/showcase.stories.tsx
@@ -39,7 +39,7 @@ import { hBarGradient } from './helpers';
const ACCENT = '#0e7490';
// ---------------------------------------------------------------------------
-// 1. Multi-series line — legend bottom-right, endpoint labels
+// 1. Multi-series line — explicit Amazon focus and endpoint labels
// ---------------------------------------------------------------------------
const multiLineSpec: ChartSpec = {
@@ -53,8 +53,18 @@ const multiLineSpec: ChartSpec = {
type: 'quantitative',
axis: { title: 'Annual revenue ($B)', format: ',.0f', grid: true },
},
- color: { field: 'company', type: 'nominal' },
+ color: {
+ field: 'company',
+ type: 'nominal',
+ scale: {
+ domain: ['Amazon', 'Apple', 'Alphabet', 'Microsoft', 'Meta'],
+ range: ['#0e7490', '#7a858f', '#7a858f', '#7a858f', '#7a858f'],
+ },
+ },
},
+ legend: { show: false },
+ endpointLabels: { content: 'label', showMarker: true },
+ seriesStyles: { Amazon: { strokeWidth: 3 } },
annotations: [
{
type: 'text',
@@ -82,14 +92,24 @@ const multiLineSpec: ChartSpec = {
const fullChromeSpec: ChartSpec = {
animation: true,
- mark: { type: 'bar', fill: hBarGradient(ACCENT) },
+ mark: 'bar',
data: [...populationByCountry.data],
encoding: {
x: { field: 'population', type: 'quantitative', axis: { title: 'Population', format: '.2~s' } },
- y: { field: 'country', type: 'nominal' },
+ y: { field: 'country', type: 'nominal', sort: 'x' },
+ color: { field: 'country', type: 'nominal', highlight: ['India', 'China'] },
},
+ legend: { show: false },
+ labels: { color: 'var(--oc-text)' },
annotations: [
- { type: 'refline', x: 1_000_000_000, label: '1 billion', style: 'dashed', stroke: '#cc4444' },
+ {
+ type: 'refline',
+ x: 1_000_000_000,
+ label: '1 billion',
+ labelPlacement: 'outside',
+ style: 'dashed',
+ stroke: '#cc4444',
+ },
],
chrome: {
eyebrow: 'Demographics',
@@ -449,7 +469,7 @@ export const Showcase = () => (
title="Horizontal bar with full chrome"
description="Every editorial chrome element at once: eyebrow, title, subtitle, source, byline, and footer wrapped around a ranked horizontal bar."
spec={fullChromeSpec}
- height={520}
+ height={600}
/>
diff --git a/examples/src/gallery/welcome.stories.tsx b/examples/src/gallery/welcome.stories.tsx
index 70865b50..fd186270 100644
--- a/examples/src/gallery/welcome.stories.tsx
+++ b/examples/src/gallery/welcome.stories.tsx
@@ -367,10 +367,18 @@ const heroSpec: ChartSpec = {
type: 'quantitative',
axis: { title: 'Annual revenue ($B)', format: ',.0f', grid: true },
},
- color: { field: 'company', type: 'nominal' },
+ color: {
+ field: 'company',
+ type: 'nominal',
+ scale: {
+ domain: ['Amazon', 'Apple', 'Alphabet', 'Microsoft', 'Meta'],
+ range: ['#0e7490', '#7a858f', '#7a858f', '#7a858f', '#7a858f'],
+ },
+ },
},
- legend: { position: 'top' },
- endpointLabels: false,
+ legend: { show: false },
+ endpointLabels: { content: 'label', showMarker: true },
+ seriesStyles: { Amazon: { strokeWidth: 3 } },
labels: { density: 'none' },
annotations: [
{
diff --git a/examples/src/geojson.d.ts b/examples/src/geojson.d.ts
new file mode 100644
index 00000000..628076be
--- /dev/null
+++ b/examples/src/geojson.d.ts
@@ -0,0 +1,4 @@
+declare module '*.geojson?raw' {
+ const source: string;
+ export default source;
+}
diff --git a/examples/src/testing/fixtures-bars.stories.tsx b/examples/src/testing/fixtures-bars.stories.tsx
index 3bcac1f6..7cf0f58d 100644
--- a/examples/src/testing/fixtures-bars.stories.tsx
+++ b/examples/src/testing/fixtures-bars.stories.tsx
@@ -142,6 +142,8 @@ export const LongAxisLabels = () => (
// ---------------------------------------------------------------------------
const simpleBarSpec: ChartSpec = {
+ // This fixture has an exact-height host, including its note reservation.
+ chromeLayout: 'subtract',
animation: true,
mark: { type: 'bar', fill: hBarGradient('#1b7fa3') },
data: [
@@ -194,7 +196,7 @@ const simpleBarSpec: ChartSpec = {
};
export const SimpleBars = () => (
-
+
);
@@ -356,7 +358,7 @@ export const TemperatureAnomaly = () => (
const populationBarSpec: ChartSpec = {
animation: true,
- mark: { type: 'bar', fill: hBarGradient('#1b7fa3') },
+ mark: 'bar',
data: [
{ country: 'India', population: 1_463_000_000 },
{ country: 'China', population: 1_410_000_000 },
@@ -371,13 +373,17 @@ const populationBarSpec: ChartSpec = {
],
encoding: {
x: { field: 'population', type: 'quantitative', axis: { title: 'Population' } },
- y: { field: 'country', type: 'nominal' },
+ y: { field: 'country', type: 'nominal', sort: 'x' },
+ color: { field: 'country', type: 'nominal', highlight: ['India', 'China'] },
},
+ legend: { show: false },
+ labels: { color: 'var(--oc-text)' },
annotations: [
{
type: 'refline',
x: 1_000_000_000,
label: '1 billion',
+ labelPlacement: 'outside',
style: 'dashed',
stroke: '#cc4444',
},
diff --git a/examples/src/testing/fixtures-lines.stories.tsx b/examples/src/testing/fixtures-lines.stories.tsx
index bcf77193..f16f6d28 100644
--- a/examples/src/testing/fixtures-lines.stories.tsx
+++ b/examples/src/testing/fixtures-lines.stories.tsx
@@ -294,7 +294,7 @@ const fiveSeriesSpec: ChartSpec = {
};
export const FiveSeries = () => (
-
+
);
@@ -603,6 +603,8 @@ export const EditorialSingleLine = () => (
// ---------------------------------------------------------------------------
const lineSpec: ChartSpec = {
+ // The fixed-height fixture includes chrome and the note reservation.
+ chromeLayout: 'subtract',
mark: 'line',
data: [
// YouTube
diff --git a/llms-full.txt b/llms-full.txt
index 068680d8..099b575f 100644
--- a/llms-full.txt
+++ b/llms-full.txt
@@ -24,7 +24,7 @@ The engine validates the spec, compiles layout and scales, and renders SVG (char
## VizSpec
-VizSpec is a union of six spec kinds, discriminated differently per kind:
+VizSpec is a union of these spec kinds, discriminated differently per kind:
- **ChartSpec**: has a `mark` field (a mark string or MarkDef object). No `type`.
- **LayerSpec**: has a `layer` array (overlay multiple charts on shared scales).
@@ -33,6 +33,7 @@ VizSpec is a union of six spec kinds, discriminated differently per kind:
- **SankeySpec**: `type: "sankey"`.
- **TileMapSpec**: `type: "tilemap"`.
- **BarListSpec**: `type: "barlist"`.
+- **GeoMapSpec**: `type: "map"`.
There is no `type: "line"` grammar. Charts use `mark`, following Vega-Lite. `mark: "line"`, not `type: "line"`.
@@ -56,7 +57,6 @@ There is no `type: "line"` grammar. Charts use `mark`, following Vega-Lite. `mar
labels?: boolean | LabelConfig, // direct data labels
legend?: LegendConfig, // { position, show, columns, ... }
theme?: ThemeConfig, // colors, fonts, spacing overrides
- darkMode?: "auto" | "force" | "off",
a11y?: { description?: string, hidden?: boolean },
description?: string, // sugar -> a11y.description (alt text)
width?: number, height?: number,
@@ -137,7 +137,7 @@ A bin plus a count aggregate is a histogram. Take both bin edges (`as: ["bin_x",
- `{ type: "text", x, y, text, subtitle?, connector?, dot?, anchor? }` - callout at a data coordinate.
- `{ type: "range", x1?, x2?, y1?, y2?, label? }` - highlighted band.
-- `{ type: "refline" | "rule", x?, y?, style?, label? }` - reference line at a value.
+- `{ type: "refline", x?, y?, style?, label?, labelPlacement? }` - reference line at a value.
## Chrome
@@ -151,7 +151,7 @@ Each accepts a string or `{ text, style?, offset? }`.
## Theme
-`theme` overrides design tokens: `{ colors?, fonts?, spacing?, borderRadius?, chrome? }`. `colors` accepts a categorical array or an object (`categorical`, `sequential`, `diverging`, `background`, `text`, `gridline`, `positive`, `negative`, ...). `darkMode` ("auto" | "force" | "off") controls automatic dark adaptation.
+`theme` overrides design tokens: `{ colors?, fonts?, spacing?, borderRadius?, chrome? }`. `colors` accepts a categorical array or an object (`categorical`, `sequential`, `diverging`, `background`, `text`, `gridline`, `positive`, `negative`, ...). Dark mode is not a spec field: pass `darkMode` ("auto" | "force" | "off") as a mount option or component prop.
## TableSpec
@@ -164,9 +164,8 @@ Each accepts a string or `{ text, style?, offset? }`.
search?: boolean,
pagination?: boolean | { pageSize: number },
stickyFirstColumn?: boolean,
- compact?: boolean,
+ density?: "condensed" | "regular" | "relaxed",
theme?: ThemeConfig,
- darkMode?: "auto" | "force" | "off",
}
```
@@ -180,10 +179,29 @@ Each accepts a string or `{ text, style?, offset? }`.
encoding?: { nodeColor?, nodeSize?, edgeColor?, edgeWidth?, nodeLabel? },
layout?: { type: "force" | "radial" | "hierarchical", clustering?, chargeStrength?, linkDistance? },
chrome?: Chrome,
- darkMode?: "auto" | "force" | "off",
}
```
+## GeoMapSpec
+
+```typescript
+{
+ type: "map",
+ geo: { features: topology, projection?: "albersUsa" | "mercator" | "equalEarth" | "identity", zoom?: boolean },
+ data: rows,
+ encoding: { key?, color?, tooltip?, text?, textPriority? },
+ points?: { data, longitude, latitude, key?, size?, color?, tooltip?, text?, textPriority? },
+ callouts?: [{ id, anchor, text, subtitle?, priority?, minZoom?, maxZoom?, offset?, maxWidth? }],
+ chrome?, legend?, theme?, fields?,
+}
+```
+
+Use `createGeoMap(container, spec)` or the `GeoMap` framework component. Labels are opt-in: `text: { field: "placeName" }`; quantitative `textPriority` orders bounded placement. Point labels and point-anchored callouts require a unique `points.key`.
+
+Callout anchors are `{ kind: "feature", id }`, `{ kind: "point", id }`, or `{ kind: "coordinate", space: "geographic" | "source", x, y }`. Feature IDs refer to topology features; point IDs refer to the point key. Geographic anchors use longitude/latitude and require a geographic projection; source anchors use the preprojected identity map's coordinate system. Do not interchange these spaces. Callout IDs must be unique.
+
+Labels retain constant text size during camera movement. Decluttered text remains in the accessible inventory; callouts that do not fit use reserved numbered notes. Complete authored inventory is included in standalone SVG descriptions. A visible place-list disclosure is a runtime `labelInventory: { container: hostElement }` option, independent of any reader panel. The host owns its position. Omission does not add visible HTML or labels, and map callouts are not Cartesian editable annotations.
+
## SankeySpec, TileMapSpec, BarListSpec
- `{ type: "sankey", data, encoding: { source, target, value }, ... }` - flow diagram.
@@ -196,7 +214,7 @@ Every chart ships a screen-reader description, a hidden data table, and keyboard
## Validation and repair
-Validate a spec before rendering with `validateSpec(spec)` from `@opendata-ai/openchart-engine`. It returns `{ valid, errors, normalized }`. Each error carries a machine-readable `code`, the offending `path`, and a repair-friendly `suggestion`. Field references are checked against the columns in the provided `data`; a misspelled field name suggests the nearest actual column ("did you mean").
+Validate a spec before rendering with `validateSpec(spec)` from `@opendata-ai/openchart-engine`. It returns `{ valid, errors, normalized }`. Each error carries a machine-readable `code`, the offending `path` as a root-relative JSON Pointer (e.g. `/encoding/y/field`, `/layer/1/data/0/value`), and a repair-friendly `suggestion`. Field references are checked against the columns in the provided `data`; a misspelled field name suggests the nearest actual column ("did you mean").
## JSON Schema
@@ -362,7 +380,7 @@ const spec = {
The `encoding` object maps data fields to visual channels. `type` tells the engine how to interpret the values: `temporal` for dates, `quantitative` for numbers, `nominal` for categories.
-The chart component fills its parent container. Set width and height on the wrapper element.
+Set the wrapper width to the desired embed width. An explicit height supplies the figure budget; omitted `chromeLayout` can grow it by the note band. Set `chromeLayout: 'subtract'` for an exact-height slot. Without a height constraint, the mount uses a 400px visualization budget and adds chrome.
See this running: [live example](https://tryopendata.github.io/openchart/?story=charts--line-and-area#single-line)
@@ -405,7 +423,7 @@ chrome: {
## Change mark type
-Swap `mark: 'line'` for any supported mark type. The encoding channels stay the same, the engine handles the rest.
+Swap `mark: 'line'` for any supported mark type. Use the channels required by the new mark: arc, waffle, and parliament use `theta` for values; positional marks use `x`/`y`. Mark-specific options must belong to the chosen mark.
```tsx
const spec = {
@@ -538,7 +556,7 @@ Dark mode adapts the theme automatically: background, text colors, gridlines, ax
## Custom theme
-Override any part of the default theme with a `ThemeConfig` object. The engine deep-merges your overrides onto the defaults, so you only specify what you want to change.
+Override any part of the default theme with a `ThemeConfig` object. Spec, provider, and component themes deep-merge in that order; nested fields survive and arrays/color pairs replace atomically. Mount options have highest precedence. Only authored overrides are stamped inline, so omitted CSS tokens remain available to host styles.
```tsx
import { Chart, VizThemeProvider } from "@opendata-ai/openchart-react";
@@ -603,7 +621,7 @@ Theme config options:
| Property | What it controls |
| -------------------- | ----------------------------------------------------- |
| `colors.categorical` | Array of CSS color strings for series differentiation |
-| `colors.background` | Chart background color |
+| `colors.background` | Chart and table background (transparent by default) |
| `colors.text` | Default text color |
| `colors.gridline` | Gridline color |
| `colors.axis` | Axis line and tick color |
@@ -848,7 +866,7 @@ const chart = createChart(
},
{
darkMode: "auto",
- responsive: true,
+ autoResize: true,
},
);
@@ -872,7 +890,7 @@ chart.destroy();
| `export(format)` | Export as `'svg'`, `'png'`, `'jpg'`, `'gif'` (animated, needs the optional `gifenc` peer), or `'csv'` |
| `destroy()` | Remove DOM elements, disconnect observers |
-Responsive mode (enabled by default) uses a `ResizeObserver` on the container, so charts resize automatically when the container changes size.
+`autoResize` (enabled by default) uses `ResizeObserver` so charts resize with their container. The spec `responsive` field controls layout adaptation, not observation. For transaction-based editing and changed defaults, see [Migrating to v9](migrating-v9.md).
### Table
@@ -894,7 +912,7 @@ const table = createTable(
pagination: { pageSize: 20 },
},
{
- responsive: true,
+ autoResize: true,
onRowClick: (row) => console.log("Clicked:", row),
onStateChange: (state) => {
// { sort, search, page }
@@ -993,12 +1011,12 @@ The primary input for standard chart types. Source: `core/src/types/spec.ts`.
| `data` | `DataRow[]` | (required) | Array of data rows. Each row is a `Record
`. Must be non-empty. |
| `encoding` | `Encoding` | (required) | Maps data fields to visual channels. See [Encoding](#encoding). |
| `chrome` | `Chrome` | `undefined` | Editorial text: title, subtitle, source, byline, footer. See [Chrome](#chrome). |
-| `chromeLayout`| `'subtract' \| 'grow'` | `'subtract'` | How chrome text interacts with the container height budget. See [Chrome layout](#chrome-layout). |
+| `chromeLayout`| `'subtract' \| 'grow'` | note-band growth | How chrome text interacts with the container height budget. See [Chrome layout](#chrome-layout). |
| `annotations` | `Annotation[]` | `undefined` | Text callouts, highlighted ranges, reference lines. See [Annotations](#annotations). |
| `labels` | `LabelConfig` | `undefined` | Data label display controls. See [Labels](#labels). |
-| `responsive` | `boolean` | `true` | Whether the chart adapts to container width via ResizeObserver. |
+| `responsive` | `boolean \| ResponsiveConfig` | `true` | Layout adaptations at compact sizes; mount `autoResize` controls ResizeObserver. |
| `theme` | `ThemeConfig` | `undefined` | Theme overrides. Deep-merged onto the default theme. See [ThemeConfig](#themeconfig). |
-| `watermark` | `boolean` | `true` | Whether to show the tryOpenData.ai watermark. Spec-level value takes precedence over mount/compile options. Defaults to `true`; auto-hides when the container is under 200px tall unless explicitly set. |
+| `watermark` | `boolean` | `true` | Whether to show the tryOpenData.ai watermark. Mount/compile options take precedence over spec and active breakpoint values. Defaults to `true`; auto-hides when the container is under 200px tall unless explicitly set. |
| `animation` | `AnimationSpec`| `undefined` | Animation configuration. `true` enables entrance + update/exit animations. See [Animation](#animation). |
| `description` | `string` | `undefined` | Alt text for the chart (Vega-Lite aligned). Sugar for `a11y.description`. Auto-generated when absent. See the [accessibility guide](accessibility.md). |
| `a11y` | `A11yConfig` | `undefined` | Accessibility overrides: `description` (custom alt text, wins over top-level `description`) and `hidden` (hide from assistive technology). |
@@ -1013,6 +1031,8 @@ A plain object with string keys. Values can be numbers, strings, dates, nulls, a
### Mark properties
+`MarkDef` is a discriminated union: each object accepts only options for its mark type. TypeScript, JSON Schema, and runtime validation enforce the same contract before sugar expansion. Extend it with an intersection type rather than an interface.
+
The `mark` field on ChartSpec accepts either a string (`'line'`) or an object with additional mark configuration. LayerSpec children are ChartSpecs, so they use `mark` too.
```ts
@@ -1112,7 +1132,7 @@ Overlay multiple chart types on shared scales. Source: `core/src/types/spec.ts`.
| `annotations` | `Annotation[]` | `undefined` | Annotations on the layered view. |
| `labels` | `LabelConfig` | `undefined` | Label display configuration. |
| `legend` | `LegendConfig` | `undefined` | Legend display configuration. |
-| `responsive` | `boolean` | `true` | Whether the chart adapts to container width. |
+| `responsive` | `boolean \| ResponsiveConfig` | `undefined` | Same as `ChartSpec.responsive`. When omitted, the first leaf's value applies. |
| `theme` | `ThemeConfig` | `undefined` | Theme overrides. |
| `watermark` | `boolean` | `true` | Whether to show the watermark. Defaults to `true`; auto-hides when the container is under 200px tall unless explicitly set. |
| `resolve` | `ResolveConfig` | `undefined` | Resolution strategy for shared vs. independent scales/axes/legends. |
@@ -1159,6 +1179,7 @@ interface Encoding {
y?: EncodingChannel;
color?: EncodingChannel;
size?: EncodingChannel;
+ theta?: EncodingChannel; // arc/waffle/parliament quantitative value
detail?: EncodingChannel;
facet?: FacetChannel; // small multiples -- see Faceting
row?: FacetChannel; // vertical facet stack -- see Faceting
@@ -1236,14 +1257,14 @@ The engine validates encoding channels at runtime using `MARK_ENCODING_RULES`. S
| `area` | temporal, ordinal (req) | quantitative (req) | nominal, ordinal (opt) | quantitative (opt) | nominal (opt) | Same encoding as line, filled region |
| `bar` | quantitative (req) | nominal, ordinal (req) | nominal, ordinal (opt) | quantitative (opt) | nominal (opt) | Horizontal bars. x = values, y = categories |
| `column` | nominal, ordinal, temporal (req) | quantitative (req) | nominal, ordinal (opt) | quantitative (opt) | nominal (opt) | Vertical bars. x = categories, y = values |
-| `pie` | -- (opt, unused) | quantitative (req) | nominal, ordinal (req) | quantitative (opt) | nominal (opt) | color = slice categories, y = slice values |
-| `donut` | -- (opt, unused) | quantitative (req) | nominal, ordinal (req) | quantitative (opt) | nominal (opt) | Same as pie with inner radius |
+| `pie` | -- (opt, unused) | -- (use theta) | nominal, ordinal (req) | quantitative (opt) | nominal (opt) | color = slice categories, theta = slice values |
+| `donut` | -- (opt, unused) | -- (use theta) | nominal, ordinal (req) | quantitative (opt) | nominal (opt) | theta = slice values; same as pie with inner radius |
| `dot` | quantitative (req) | nominal, ordinal (req) | nominal, ordinal (opt) | quantitative (opt) | nominal (opt) | Dot plot. x = values, y = categories |
| `scatter` | quantitative (req) | quantitative (req) | nominal, ordinal (opt) | quantitative (opt) | nominal (opt) | Both axes quantitative. size creates bubble chart |
| `beeswarm` | quantitative or nominal (opt) | quantitative or nominal (opt) | nominal, ordinal, quantitative (opt) | quantitative (opt) | nominal (opt) | One position is the quantitative value axis; the other (nominal/ordinal) splits into grouped lanes |
| `range` | quantitative or nominal (req) | quantitative or nominal (req) | nominal, ordinal (opt) | -- | nominal (opt) | Dumbbell/arrow/range-bar. x+x2 (horizontal) or y+y2 (vertical); orientation-dependent second endpoint |
-| `waffle` | -- (opt, unused) | quantitative (req, via `theta`) | nominal, ordinal (req) | -- | nominal (opt) | Unit grid. `theta` (alias for y) = share, color = category |
-| `parliament` | -- (opt, unused) | quantitative (req, via `theta`) | nominal, ordinal (req) | -- | nominal (opt) | Hemicycle seats. `theta` (alias for y) = seat count, color = party |
+| `waffle` | -- (opt, unused) | -- (use theta) | nominal, ordinal (req) | -- | nominal (opt) | Unit grid. `theta` = share, color = category |
+| `parliament` | -- (opt, unused) | -- (use theta) | nominal, ordinal (req) | -- | nominal (opt) | Hemicycle seats. `theta` = seat count, color = party |
| `histogram` | quantitative (req) | quantitative (opt) | nominal, ordinal (opt) | quantitative (opt) | nominal (opt) | Raw values on x; y defaults to the per-bin count. Color groups overlap rather than dodge |
| `density` | quantitative (req) | -- (computed) | nominal, ordinal (opt) | -- | nominal (opt) | KDE over raw x values; y is the estimated density |
| `calendar` | temporal (req) | -- (not allowed) | quantitative (req) | -- | nominal (opt) | Calendar heatmap. x = daily date, color = per-day value; owns its own geometry |
@@ -1254,6 +1275,8 @@ The engine validates encoding channels at runtime using `MARK_ENCODING_RULES`. S
**y channel**: Vertical position. Quantitative values for most types. Nominal/ordinal categories for bar and dot (horizontal layout).
+**theta channel**: Quantitative part-to-whole values for arc, waffle, and parliament. Their former authored `y` alias is rejected; use `theta`.
+
**color channel**: Series differentiation. Assigns colors from the categorical palette. Required for pie/donut (defines slices). Optional for all others (creates multi-series with legend).
**size channel**: Mark size. Creates bubble charts when applied to scatter. Maps a quantitative field to the dot **radius in pixels**, area-proportionally (sqrt curve); the default range is [3, 30] and `scale.range` overrides it in radius px. A constant lives at `mark.size`, also a radius (default 5, typical 2-12). Note this differs from Vega-Lite, where point `size` is an **area** in px² — a VL size of 110 is `mark.size: 6` here (r = √(area/π)); validation rejects radii above 50 with the conversion.
@@ -1347,12 +1370,12 @@ Each field accepts either a plain string or a `ChromeText` object for style over
| Field | Position | Default style |
| ---------- | ------------------------------- | ------------------------------------------------------------ |
-| `eyebrow` | Above title | 11px, semibold, uppercase, theme accent — leading accent dot |
-| `title` | Top, above chart | 22px, bold (700), `#333333` |
-| `subtitle` | Below title | 15px, normal (400), `#666666` |
-| `source` | Below chart area | 12px, normal (400), `#999999` |
-| `byline` | Below source | 12px, normal (400), `#999999` |
-| `footer` | Below byline | 12px, normal (400), `#999999` |
+| `eyebrow` | Above title | 11px, medium (500), uppercase, theme accent — leading accent dot |
+| `title` | Top, above chart | 26px, semibold (600), `#09090b` |
+| `subtitle` | Below title | 14px, normal (400), `#71717a` |
+| `source` | Below chart area | 12px, normal (400), `#71717a` |
+| `byline` | Below source | 12px, normal (400), `#71717a` |
+| `footer` | Below byline | 12px, normal (400), `#71717a` |
| `brand` | Right-aligned on the source row | 12px, suppresses the default watermark |
**KPI metric row** is a separate top-level field on chart specs, not part of `Chrome`. Use `metrics: Metric[]` on the chart spec for a horizontal row of label+value cells rendered between the subtitle and the chart area. Each cell can carry a delta and a secondary value. See `Metric` in `core/src/types/spec.ts` for the full shape.
@@ -1406,7 +1429,8 @@ The top-level `chromeLayout` property controls how chrome text height interacts
| Value | Behavior |
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
-| `'subtract'` | Default. The container height is fixed and chrome text (title, subtitle, source) is subtracted from it, shrinking the plot. Historical behavior. |
+| omitted | Notes and the series summary grow the figure; title, subtitle, source, and other chrome subtract from the container height budget. |
+| `'subtract'` | Keep the exact outer height; all chrome and notes subtract from it, shrinking the plot. |
| `'grow'` | The container height is treated as the plot budget. The rendered SVG grows taller by the measured chrome height, so the plot keeps its full budget (minus axis and padding overhead) no matter how many lines the title wraps to. |
Reach for `'grow'` on fixed-height article or blog charts where a long title on a narrow phone must not compress the plot.
@@ -1421,7 +1445,7 @@ Reach for `'grow'` on fixed-height article or blog charts where a long title on
}
```
-`chromeLayout: 'grow'` is a no-op for faceted (small-multiples) specs in this version and falls back to `'subtract'`. It is also honored by bar list, sankey, map, and tilemap specs via the spec or compile option.
+`chromeLayout: 'grow'` supports faceted charts and layers; engine-added height is counted once. Read `layout.dimensions.height` for the final height and `layout.chrome.grownHeight` for the added height. The default note-growth policy is internal: `'notes'` is not a public spec value. Bar list, Sankey, map, and tilemap also honor the public spec/compile policies.
---
@@ -1458,6 +1482,8 @@ Carrying both bin edges matters: the end edge goes to `x2`, which is what puts t
Data-positioned overlays. Source: `core/src/types/spec.ts`.
+Use unique annotation `id` values for durable selection and editing. Notes preserve complete primary/subtitle text and rich spans; their numbers follow source text-annotation order before filtering, so gaps are intentional. Notes and markers support linked focus and click/tap emphasis.
+
Three annotation types are available, discriminated by the `type` field:
### TextAnnotation
@@ -1471,15 +1497,16 @@ A callout label positioned at a data coordinate.
| `y` | `string \| number` | (required) | Y-axis data value or position. |
| `text` | `string` | (required) | The annotation text. Supports `\n` for line breaks and `**bold**` spans. |
| `subtitle` | `string` | `undefined` | Optional muted second-tone text rendered below `text`. Use for supporting context (methodology, source, etc.). Supports `**bold**` too. |
-| `maxWidth` | `number \| null` | unlimited | Optional positive wrapping width in pixels; null disables wrapping. |
-| `subtitleFontSize` | `number` | `round(fontSize * 0.85)` | Optional positive subtitle size in pixels. |
+| `maxWidth` | `number \| null` | `min(240, plotWidth - 2 * padding)`, at least 1px | Positive display wrapping width in pixels; null disables wrapping. |
+| `subtitleFontSize` | `number` | `round(fontSize * 12 / 13)` | Optional positive subtitle size in pixels. |
| `dot` | `boolean \| AnnotationDot` | see below | Open-ring marker on the data point. Appears by default on non-arrowed connectors. `false` for none, an object to restyle. |
-| `label` | `string` | `undefined` | Additional label text. |
| `fontSize` | `number` | `13` | Font size override in pixels. |
-| `fontWeight` | `number` | `400` (`700` with a `subtitle`) | Font weight override. |
+| `priority` | `number` | source order | Lower values get placement and compact-inline preference; ties retain source order. |
+| `responsive` | `boolean` | `true` | `false` pins the annotation inline and exempts it from thinning/count limits. |
+| `fontWeight` | `number` | `400` | Font weight override. |
| `offset` | `AnnotationOffset` | `undefined` | Pixel offset from computed position. `{ dx?: number, dy?: number }`. |
| `anchor` | `AnnotationAnchor` | `'auto'` | Label placement direction: `'top'`, `'bottom'`, `'left'`, `'right'`, `'auto'`. |
-| `connector` | `boolean \| ConnectorType \| ConnectorConfig` | `true` | `'straight'`, `'curve'`, or `'drop-line'`; `{ type, arrow? }` for explicit arrow control; `false` disables it. |
+| `connector` | `boolean \| ConnectorType \| ConnectorConfig` | `true` | `'straight'`, `'curve'`, or `'drop-line'`; `{ type, arrow?, bend? }` for explicit control; `false` disables it. |
| `connectorOffset` | `{ from?, to? }` | `undefined` | Per-endpoint pixel offsets for the connector line. |
| `background` | `string \| true` | `undefined` | Background plate behind the text, for readability over chart lines. `true` follows the theme surface. |
| `fill` | `string` | theme `annotationFill` | Fill color. |
@@ -1487,7 +1514,7 @@ A callout label positioned at a data coordinate.
| `opacity` | `number` | `1` | Opacity (0 to 1). |
| `zIndex` | `number` | `0` | Render ordering. Higher values render on top. |
-#### Text: line breaks, bold spans, and the lede rule
+#### Text: wrapping, line breaks, and bold spans
Curve connectors accept optional `bend: -1..1`; omitted or zero preserves the
existing automatic curve. Other connector types ignore `bend` with a warning.
@@ -1503,14 +1530,14 @@ An authored `offset` object pins placement even when both values are zero.
Only matched pairs turn bold. An unmatched `**` (and an empty `****`) renders literally, so specs with stray asterisks aren't mangled. Bold spans are measured at weight 700, so bounds, collision nudging, and connector exits all account for them.
-When a `subtitle` is present and you set no `fontWeight`, the primary text resolves to weight 700 and the subtitle stays 400. That's the lede + context stack ("Feb. 25" over "2015 maximum"). Setting `fontWeight` explicitly opts out.
+Primary text and subtitles default to weight 400. Set `fontWeight: 700` for a bold primary line or use `**bold**` for selective emphasis; adding a subtitle does not change the primary weight.
#### Connectors
-The connector leaves the text block (primary text plus subtitle, treated as one box) on the side facing the data point, with a 6px standoff gap, and stops short of the marker at the other end. Two voices:
+The connector leaves the text block (primary text plus subtitle, treated as one box) on the side facing the data point, with a 6px standoff gap, and stops short of the marker at the other end. A curved connector is shaped to the data point and then pulled back along its end tangent, so the tail still aims at the marker. Two voices:
-- **Emphasis.** An arrowed connector (`'curve'` defaults to `arrow: true`) is drawn in the label's text ink with a small open-V arrowhead, so the gesture reads as part of the sentence.
-- **Quiet.** A non-arrowed straight leader or a `'drop-line'` is a gray hairline. It points without competing with the words.
+- **Emphasis.** An explicitly arrowed connector (`{ type: 'curve', arrow: true }`) is drawn in the label's text ink with a small open-V arrowhead, so the gesture reads as part of the sentence.
+- **Quiet.** A non-arrowed curve, straight leader, or `'drop-line'` is a gray hairline. It points without competing with the words.
Connectors are dropped when they'd be noise rather than signal: a leader shorter than 8px, or one whose data point sits inside the text block. The marker still renders. A default annotation sits far enough off its point (28px) to draw a real leader without any `offset` authoring; you only lose the line by pulling the label back onto its own marker.
@@ -1519,11 +1546,11 @@ Connectors are dropped when they'd be noise rather than signal: a leader shorter
| Field | Type | Default | Description |
| ------------- | -------- | --------------------------- | --------------------------------- |
| `radius` | `number` | `4` | Circle radius in pixels. |
-| `fill` | `string` | theme background | Interior fill (the "open ring"). |
+| `fill` | `string` | theme `annotationMarkerFill` (neutral surface by default) | Interior fill (the "open ring"). |
| `stroke` | `string` | the connector's stroke | Ring color. |
| `strokeWidth` | `number` | `1.5` | Ring width in pixels. |
-The marker sits on the data point itself, not on the pulled-back connector tip. You get one by default when a connector is enabled and carries no arrowhead, including drop-lines. An arrowed connector gets no marker unless you ask for one, since the arrowhead already marks the spot. `dot: false` is always bare; an explicit `dot` always wins.
+The marker sits on the data point itself, not on the pulled-back connector tip. You get one by default when a connector is enabled and carries no arrowhead, including drop-lines. An arrowed connector gets no marker unless you ask for one, since the arrowhead already marks the spot. `dot: false` is always bare; an explicit `dot` always wins. A default dot that lands on a line's endpoint marker takes the marker's fill and stroke, so the two draw as one ring.
### RangeAnnotation
@@ -1540,9 +1567,9 @@ A highlighted band or rectangle.
| `label` | `string` | `undefined` | Range label text. |
| `labelOffset` | `AnnotationOffset` | `undefined` | Pixel offset for the label. `{ dx?: number, dy?: number }`. |
| `labelAnchor` | `AnnotationAnchor` | `'auto'` | Label placement direction. |
-| `fontSize` | `number` | `11` | Font size override for the range label (px). |
+| `fontSize` | `number` | theme `small` (12px) | Font size override for the range label (px). |
| `fontWeight` | `number` | `500` | Font weight override for the range label. |
-| `fill` | `string` | theme `annotationFill` | Fill color. |
+| `fill` | `string` | theme `annotationRangeFill` (`#f0c040`) | Fill color. |
| `stroke` | `string` | `undefined` | Stroke color. |
| `opacity` | `number` | `1` | Opacity (0 to 1). |
| `zIndex` | `number` | `0` | Render ordering. |
@@ -1567,16 +1594,18 @@ A horizontal or vertical reference line.
| `strokeWidth` | `number` | `1` | Line width in pixels. |
| `labelOffset` | `AnnotationOffset` | `undefined` | Pixel offset for the label. |
| `labelAnchor` | `AnnotationAnchor` | `'auto'` | Label placement direction. |
-| `labelPlacement` | `'auto' \| 'start' \| 'end' \| 'outside'` | legacy anchor behavior | Opt-in screen-space end selection, automatic containment, or a reserved outside gutter/lane. `labelAnchor` selects the side; an authored offset pins the base. |
-| `fontSize` | `number` | `11` | Font size override for the label (px). |
+| `labelPlacement` | `'auto' \| 'start' \| 'end' \| 'outside'` | `'auto'` unless `labelAnchor` is authored | Screen-space end selection, automatic containment, or a reserved outside gutter/lane. `labelAnchor` selects the side; an authored offset pins the base. |
+| `fontSize` | `number` | theme `small` (12px) | Font size override for the label (px). |
| `fontWeight` | `number` | `400` | Font weight override for the label. |
| `fill` | `string` | `undefined` | Fill color. |
-| `stroke` | `string` | theme `annotationText` | Stroke/line color. |
+| `stroke` | `string` | theme `annotationRule` | Stroke/line color. |
| `opacity` | `number` | `1` | Opacity (0 to 1). |
| `zIndex` | `number` | `0` | Render ordering. |
Provide `x` for a vertical line or `y` for a horizontal line. Both can be set for a crosshair.
+With `labelPlacement: 'outside'`, horizontal lines reserve a right lane as wide as the widest wrapped label (capped at 160px or 25% of the width). Vertical lines share a lane above the plot: labels whose extents don't overlap pack into one row, and whole rows are admitted while the lane stays within 25% of the height. A label whose row doesn't fit falls back to automatic placement inside the plot with a warning. The lane shrinks before the title or subtitle does.
+
### Annotation example
```ts
@@ -1845,124 +1874,80 @@ For line/area marks, mid-morph interruption uses a crossfade from the frozen int
## ThemeConfig
-User-facing theme overrides. Source: `core/src/types/spec.ts`.
+Public authored theme overrides live in `core/src/types/spec.ts`. Every field is optional. Use `ThemeConfig` for input and `ResolvedTheme` for compiled output; the former public `Theme` alias is removed.
-All fields are optional. The engine deep-merges these onto `DEFAULT_THEME`. You only specify what you want to change.
+`mergeThemeConfig()` deep-merges authored layers before `resolveTheme()` supplies defaults. Component theme props override provider themes; mount/compile options override the spec. In a `LayerSpec`, chrome, axes and legend use the layer theme (falling back to the first leaf's); each leaf's marks use its theme merged over the layer theme. Nested objects merge, arrays and `{ light, dark }` TokenValues replace atomically, and null replaces. `colors: string[]` is shorthand for `colors.categorical`.
```ts
-interface ThemeConfig {
- colors?: {
- categorical?: string[];
- categoricalFill?: string[];
- sequential?: Record;
- diverging?: Record;
- background?: string;
- text?: string;
- gridline?: string;
- hairline?: string;
- axis?: string;
- };
- fonts?: {
- family?: string;
- mono?: string;
- };
- spacing?: {
- padding?: number;
- chromeGap?: number;
- };
- rule?: { color: TokenValue; width: number; thickness: number };
- borderRadius?: number;
-}
-```
+import { broadsheet, mergeThemeConfig } from '@opendata-ai/openchart-core';
+import type { ThemeConfig } from '@opendata-ai/openchart-core';
-### Color overrides
-
-| Field | Type | Default | Description |
-| -------------------- | -------------------------- | --------------------------------- | ----------------------------------------------------------------- |
-| `colors.categorical` | `string[]` | 8-color palette | Categorical palette for nominal data. Array of CSS color strings. |
-| `colors.categoricalFill` | `string[]` | copied from `categorical` | Quieter palette used for area-filling marks (bar, area, arc, waffle, calendar, rect). Set it only to control fills and strokes independently; leaving it unset copies `categorical` so a custom palette never drifts between the two. |
-| `colors.sequential` | `Record` | `{ blue, green, orange, purple }` | Sequential palettes keyed by name. |
-| `colors.diverging` | `Record` | `{ redBlue, brownTeal }` | Diverging palettes keyed by name. |
-| `colors.background` | `string` | `'#ffffff'` | Visualization background color. |
-| `colors.text` | `string` | `'#1d1d1d'` | Default text color. |
-| `colors.gridline` | `string` | `'#e8e8e8'` | Gridline color. |
-| `colors.hairline` | `string` | `'rgba(0,0,0,0.14)'` | Structural line color for axis lines and separators. Distinct from `gridline`, which is lighter. |
-| `colors.axis` | `string` | `'#888888'` | Axis line and tick color. |
+const theme: ThemeConfig = mergeThemeConfig(broadsheet, {
+ fonts: { sizes: { small: 12, axisTick: 12 } },
+ chrome: { title: { fontSize: 28 } },
+ rule: { width: 80 },
+});
+```
-### Font overrides
+Authored `chrome..fontSize` takes precedence over propagation from `fonts.sizes`, including across theme layers. Theme chrome values are objects such as `{ color: '#333' }`; string color shorthand is removed. Spec chrome text shorthand (`chrome: { title: 'A title' }`) remains valid.
-| Field | Type | Default | Description |
-| -------------- | -------- | ----------------------------------------- | ----------------------------------- |
-| `fonts.family` | `string` | `'Inter, -apple-system, ..., sans-serif'` | Primary font family. |
-| `fonts.mono` | `string` | `'"JetBrains Mono", ..., monospace'` | Monospace font for tabular numbers. |
+### Color overrides
-### Spacing overrides
+Single colors accept `TokenValue` (`string | { light: string; dark: string }`). Palette arrays contain color strings. These are light-mode defaults unless a pair is shown:
-| Field | Type | Default | Description |
-| ------------------- | -------- | ------- | ---------------------------------------- |
-| `spacing.padding` | `number` | `12` | Padding inside the chart container (px). |
-| `spacing.chromeGap` | `number` | `4` | Gap between chrome elements (px). |
+| Field | Default | Description |
+|---|---|---|
+| `colors.categorical` | Six-hue palette | Nominal-series stroke palette |
+| `colors.categoricalFill` | Default fill palette; inherits an authored categorical palette when omitted | Quieter fill colors for area-filling marks |
+| `colors.sequential` / `diverging` | Named palette maps | Numeric color scales |
+| `colors.background` | `transparent` | Visualization background; opaque surfaces derive from the neutral surface |
+| `colors.text` | `#09090b` | Main text ink |
+| `colors.gridline` | `rgba(0,0,0,0.08)` | Grid lines |
+| `colors.hairline` | `rgba(0,0,0,0.14)` | Structural separators |
+| `colors.axis` | `#71717a` | Axis lines and tick text |
+| `colors.annotationLabel` | `#333333` | Primary callout ink |
+| `colors.annotationMuted` | `#6b7280` | Subtitle and note ink |
+| `colors.annotationLeader` | `#8f8f8f` | Quiet connector ink |
+| `colors.annotationRule` | `#888888` / `#9ca3af` | Reference line ink, light/dark |
+| `colors.annotationRangeFill` | `#f0c040` | Range fill before opacity |
+| `colors.annotationMarkerFill` / `annotationPlate` | Resolved neutral surface | Marker interior and label plate; explicit colors win |
+
+Generic `annotationFill` and `annotationText` retain their other uses. See [Theming tokens](theming-tokens.md) for the complete token and dark-mode mapping.
+
+### Fonts, chrome, and spacing
+
+| Field | Default | Description |
+|---|---|---|
+| `fonts.family` | Inter Variable, Inter, system sans-serif | Primary font family |
+| `fonts.mono` | JetBrains Mono, Fira Code, Cascadia Code, monospace | Tabular/monospace family |
+| `fonts.sizes.title` / `subtitle` | 26 / 14 | General title sizes |
+| `fonts.sizes.body` / `small` / `axisTick` | 13 / 12 / 12 | Body, small, and tick sizes |
+| `fonts.sizes.metricLabel` / `metricValue` | 11 / 22 | Metric row sizes |
+| `fonts.weights.normal` / `medium` / `semibold` / `bold` | 400 / 500 / 600 / 700 | Weight ladder |
+| `chrome.eyebrow` | 11px, 500, `#06b6d4`, 1.4 line-height | Eyebrow style |
+| `chrome.title` | 26px, 600, `#09090b`, 1.15 line-height | Title style |
+| `chrome.subtitle` | 14px, 400, `#71717a`, 1.45 line-height | Subtitle style |
+| `chrome.source` / `byline` / `footer` | 12px, 400, `#71717a`, 1.4 line-height | Attribution styles |
+| `spacing.padding` / `chromeGap` | 20 / 6 | Container padding / gap between chrome elements |
+| `spacing.chromeToChart` / `chartToFooter` | 16 / 12 | Gaps above and below the chart |
+| `spacing.axisMargin` | 6 | Axis margin |
+| `borderRadius` | 8 | Containers and tooltips |
### The editorial rule
-`rule` draws a short colored bar above the chrome block — the Economist/FT
-masthead device. Omit it (the default on every preset but `broadsheet`) for no
-rule.
+`rule` draws a short colored bar above top chrome. Omission inherits a preset rule, or draws none on the default theme. `rule: null` removes an inherited rule; partial objects preserve unmodified fields.
-```ts
-theme: {
- rule: { color: { light: '#e3120b', dark: '#f4463f' }, width: 40, thickness: 3 },
-}
-```
-
-| Field | Type | Default | Description |
-| ---------------- | ------------ | ----------- | ------------------------------------------------------------------ |
-| `rule.color` | `TokenValue` | (required) | Rule color. A `TokenValue` accepts a `{ light, dark }` pair, so it needs no dark-mode branch. |
-| `rule.width` | `number` | (required) | Rule length in pixels. |
-| `rule.thickness` | `number` | (required) | Rule height in pixels. `computeChrome` reserves `thickness + chromeGap` above the eyebrow/title. |
+| Field | Fallback when a rule is authored without a preset | Description |
+|---|---|---|
+| `rule.color` | Theme text ink | TokenValue color, including light/dark pairs |
+| `rule.width` | 40px | Rule length |
+| `rule.thickness` | 3px | Rule height; chrome reserves thickness plus chromeGap |
No top chrome means no rule.
-### Other
-
-| Field | Type | Default | Description |
-| -------------- | -------- | ------- | -------------------------------------------------------- |
-| `borderRadius` | `number` | `4` | Border radius for the chart container and tooltips (px). |
-
-### DEFAULT_THEME reference
-
-The full default theme (from `core/src/theme/defaults.ts`):
-
-| Property | Value |
-| ------------------------ | ------------------------------------------------------------------------ |
-| `colors.background` | `#ffffff` |
-| `colors.text` | `#1d1d1d` |
-| `colors.gridline` | `#e8e8e8` |
-| `colors.axis` | `#888888` |
-| `colors.annotationFill` | `rgba(0,0,0,0.04)` |
-| `colors.annotationText` | `#555555` |
-| `fonts.family` | Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif |
-| `fonts.mono` | "JetBrains Mono", "Fira Code", "Cascadia Code", monospace |
-| `fonts.sizes.title` | 22 |
-| `fonts.sizes.subtitle` | 15 |
-| `fonts.sizes.body` | 13 |
-| `fonts.sizes.small` | 11 |
-| `fonts.sizes.axisTick` | 11 |
-| `fonts.weights.normal` | 400 |
-| `fonts.weights.medium` | 500 |
-| `fonts.weights.semibold` | 600 |
-| `fonts.weights.bold` | 700 |
-| `spacing.padding` | 12 |
-| `spacing.chromeGap` | 4 |
-| `spacing.chromeToChart` | 8 |
-| `spacing.chartToFooter` | 8 |
-| `spacing.axisMargin` | 6 |
-| `borderRadius` | 4 |
-| `chrome.title` | 22px, 700 weight, `#333333`, 1.3 line-height |
-| `chrome.subtitle` | 15px, 400 weight, `#666666`, 1.4 line-height |
-| `chrome.source` | 12px, 400 weight, `#999999`, 1.3 line-height |
-| `chrome.byline` | 12px, 400 weight, `#999999`, 1.3 line-height |
-| `chrome.footer` | 12px, 400 weight, `#999999`, 1.3 line-height |
+The named presets are `essay`, `wire`, `broadsheet`, and `terminal`. Omit `theme` or pass `{}` for the default; `editorial` is removed. `clarity` is deprecated and contains only its background, axis, and eyebrow colors. `legacyEditorial` retains v8 typography for migration.
+
+Only authored theme overrides and derived values are stamped inline. Omitted CSS tokens remain overridable by host styles, including `--oc-border-radius`; removing an override restores the host's previous inline value. Export captures live token values.
---
@@ -2005,9 +1990,8 @@ Input for data table visualizations. Source: `core/src/types/spec.ts`.
| `striped` | `boolean` | `false` | Zebra striping on body rows. Hairlines only by default. |
| `sort` | `SortState` | first inline-bar column, descending | Initial sort applied before the user touches a header. `{ column: string, direction: 'asc' \| 'desc' }`. |
| `totalRow` | `boolean \| { label?: string }` | `false` | Sticky footer summing every quantitative column over the filtered rows (the whole result set, not just the current page). `true` uses the label `'Total'`. |
-| `compact` | `boolean` | `false` | **Deprecated.** Alias for `density: 'condensed'`; still honored, with a warning. |
| `responsive` | `boolean` | `true` | Whether the table adapts to container width. |
-| `watermark` | `boolean` | `true` | Whether to show the tryOpenData.ai watermark. Spec-level value takes precedence over mount/compile options. |
+| `watermark` | `boolean` | `true` | Whether to show the tryOpenData.ai watermark. Mount/compile options take precedence over spec and active breakpoint values. |
### ColumnConfig
@@ -2123,8 +2107,13 @@ Pass these through `MountOptions` (vanilla) or `ChartProps` (React).
| `onMarkClick` | `(event: MarkEvent) => void` | User clicks a data mark (bar, point, line, arc). |
| `onMarkHover` | `(event: MarkEvent) => void` | Mouse enters a data mark. |
| `onMarkLeave` | `() => void` | Mouse leaves a data mark. |
+| `onEdit` | `(transaction: EditTransaction) => void` | One committed authored edit; apply atomically with `applyEditTransaction` and retain its inverse for undo. |
| `onLegendToggle` | `(series: string, visible: boolean) => void` | User clicks a legend entry to show/hide a series. |
-| `onAnnotationClick` | `(annotation: Annotation, event: MouseEvent) => void` | User clicks an annotation element. |
+| `onAnnotationClick` | `(annotation: Annotation, event: MouseEvent, element: ElementRef) => void` | User clicks an annotation element. |
+| `onSelect` | `(element: ElementRef) => void` | An element is selected by click, Tab, or `select()`. Requires `interaction` `'select'` or `'edit'`. |
+| `onDeselect` | `(element: ElementRef) => void` | The selected element is deselected. |
+
+The `interaction` mount option (`'read' | 'select' | 'edit'`) gates selection and editing; when omitted, `onEdit` implies `'edit'` and `onSelect`/`onDeselect` imply `'select'`. Every `ElementRef` carries a JSON Pointer `path`. See [Editing and keyboard navigation](migrating-v9.md#editing-and-keyboard-navigation) for transaction persistence and cancelled-gesture behavior. See [Interaction mode](integration-guide.md#interaction-mode) for the full table.
### MarkEvent
@@ -2167,7 +2156,7 @@ Graphs render force-directed network visualizations on canvas. They support node
| `chrome` | `Chrome` | `undefined` | Editorial text. |
| `annotations` | `Annotation[]` | `undefined` | Annotations. |
| `theme` | `ThemeConfig` | `undefined` | Theme overrides. |
-| `watermark` | `boolean` | `true` | Whether to show the tryOpenData.ai watermark. Spec-level value takes precedence over mount/compile options. |
+| `watermark` | `boolean` | `true` | Whether to show the tryOpenData.ai watermark. Mount/compile options take precedence over spec and active breakpoint values. |
### GraphEncoding
@@ -2230,7 +2219,6 @@ Sankey diagrams show flows between stages. Each data row represents a link from
| `linkStyle` | `SankeyLinkColor` | `'gradient'`| Link coloring: `'gradient'`, `'source'`, `'target'`, `'neutral'`. |
| `linkOpacity` | `number` | `0.5` | Link fill opacity (0-1). Defaults to 0.75 in dark mode. |
| `nodeLabelAlign`| `string` | `'auto'` | Label placement: `'auto'`, `'left'`, `'right'`. |
-| `valueFormat` | `string` | `undefined` | d3-format string for values in tooltips and ARIA labels (e.g. `"$,.0f"`, `"~s"`). |
| `chrome` | `Chrome` | `undefined` | Editorial text (title, subtitle, source, byline, footer). |
| `legend` | `LegendConfig` | `undefined` | Legend display configuration. |
| `theme` | `ThemeConfig` | `undefined` | Theme overrides. |
@@ -2295,7 +2283,6 @@ Maps render TopoJSON geometries as SVG, join tabular data rows to features by id
| `theme` | `ThemeConfig` | `undefined` | Theme overrides. |
| `watermark` | `boolean` | `true` | Whether to show the tryOpenData.ai watermark. |
| `animation` | `AnimationSpec` | `undefined` | Entrance animation configuration. |
-| `valueFormat` | `string` | `undefined` | Deprecated: use `encoding.color.format` instead. d3-format string for color values in tooltips and the legend (e.g. `".1f"`). `encoding.color.format` wins when both are set. |
### GeoMapGeo
@@ -2434,7 +2421,7 @@ Every state renders as an equal-size square in a fixed 12x8 grid, so small state
| `theme` | `ThemeConfig` | `undefined` | Theme overrides. |
| `watermark` | `boolean` | `true` | Whether to show the tryOpenData.ai watermark. |
| `animation` | `AnimationSpec` | `undefined` | Entrance animation configuration. |
-| `valueFormat` | `string` | `undefined` | Deprecated: use `encoding.value.format` instead. d3-format string for tile values, legend labels, and tooltips. |
+| `valueFormat` | `string` | `undefined` | Tilemap fallback d3-format string for values, legend labels, and tooltips; `encoding.value.format` takes precedence. |
### TileMapEncoding
@@ -2449,7 +2436,7 @@ Every state renders as an equal-size square in a fixed 12x8 grid, so small state
- **Quantitative mode** renders a gradient legend bar with min/max labels below the grid. **Categorical mode** (string values, a `color` channel, or a `colors` map) renders swatch rows instead.
- States missing from the data render as empty tiles, keeping their place on the grid.
-- **Number formatting:** set `encoding.value.format` (d3-format string, e.g. `".1f"`, `"$,.0f"`, `"~s"`). It applies to tile values, legend labels, and tooltips, and wins over the deprecated top-level `valueFormat`. With record-map data and no explicit encoding, `valueFormat` still applies (with a deprecation warning).
+- **Number formatting:** set `encoding.value.format` (d3-format string, e.g. `".1f"`, `"$,.0f"`, `"~s"`). It applies to tile values, legend labels, and tooltips, and wins over top-level `valueFormat`. With record-map data and no explicit encoding, `valueFormat` remains supported.
- **`scale.scheme` fails validation on tilemap encoding channels.** The tilemap compile path never reads it; the top-level `palette` property is the mechanism tilemaps use for color.
### Tilemap example
@@ -2602,7 +2589,7 @@ const spec: ChartSpec = {
{ category: "Tablet", share: 7 },
],
encoding: {
- y: { field: "share", type: "quantitative" },
+ theta: { field: "share", type: "quantitative" },
color: { field: "category", type: "nominal" },
},
chrome: { title: "Traffic by device" },
@@ -2622,7 +2609,7 @@ const spec: ChartSpec = {
{ status: "Blocked", count: 5 },
],
encoding: {
- y: { field: "count", type: "quantitative" },
+ theta: { field: "count", type: "quantitative" },
color: { field: "status", type: "nominal" },
},
chrome: { title: "Task status" },
@@ -2762,9 +2749,9 @@ All builders accept field names as strings (auto-infer type from data) or full `
| `annotations` | `Annotation[]` | Annotations. |
| `responsive` | `boolean` | Responsive behavior. |
| `theme` | `ThemeConfig` | Theme overrides. |
-| `watermark` | `boolean` | Whether to show the tryOpenData.ai watermark. Spec-level value takes precedence over mount/compile options; defaults to `true`. |
+| `watermark` | `boolean` | Whether to show the tryOpenData.ai watermark. Mount/compile options take precedence over the spec; defaults to `true`. |
-`FieldRef` is `string | EncodingChannel`. When a string is provided, `inferFieldType()` samples up to 20 data values to determine the encoding type (quantitative, temporal, or nominal).
+`FieldRef` is `string | EncodingChannel`. When a string is provided, `inferFieldType()` uses the shared builder/compiler inference: sample up to 50 rows, ignore nulls, and require more than 80% agreement. Numeric strings are considered before dates, so a column of `'2020'` values is quantitative. Set the channel type explicitly when years should be ordinal or temporal.
### Builder example
@@ -2795,12 +2782,12 @@ const result = validateSpec(spec);
### ValidationError
-| Field | Type | Description |
-| ------------ | --------------------- | ------------------------------------------------------------- |
-| `message` | `string` | Human-readable error description. |
-| `path` | `string \| undefined` | Dot-path to the field that failed, e.g. `'encoding.x.field'`. |
-| `code` | `ValidationErrorCode` | Machine-readable error code. |
-| `suggestion` | `string` | Actionable fix suggestion. |
+| Field | Type | Description |
+| ------------ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
+| `message` | `string` | Human-readable error description. |
+| `path` | `string` | Root-relative RFC 6901 JSON Pointer to the value that failed, e.g. `'/encoding/x/field'` or `'/layer/1/data/0/value'`. The root is `''`. |
+| `code` | `ValidationErrorCode` | Machine-readable error code. |
+| `suggestion` | `string` | Actionable fix suggestion. |
### ValidationErrorCode
@@ -2826,14 +2813,14 @@ const result = validateSpec(spec);
## Authoring foundation additions
-The separate [OpenStudio application](https://github.com/tryopendata/openstudio) consumes these additive interfaces. Existing v8 visual and animation defaults remain unchanged.
+These library interfaces support authoring and reader applications. OpenStudio adoption is separate; see [Migrating to v9](migrating-v9.md) for the changed defaults and removed APIs.
-- **Theme:** import `clarity` for white/ink surfaces, Inter titles and 12px axes; `legacyEditorial` restores documented v8 typography without freezing layout behavior.
+- **Theme:** v9 defaults include 12px axes/small text and larger chrome gaps. `clarity` is deprecated and retains only background, axis, and eyebrow colors. `legacyEditorial` restores v8 typography; `migrateSpecV8` can materialize the wider v8 styling contract without freezing layout behavior.
- **Motion:** `animation: 'fluid' | 'subtle' (omit the preset for v8 timing)`; explicit phases override their preset. A phase can use `spring: { stiffness: 320, damping: 36, mass: 1 }` or the existing duration/easing configuration. Combining a spring with duration/easing in the same phase is an error. Quantitative springs are bounded, with a finite terminal state. Omitted/false animation remains disabled.
- **Responsiveness:** `responsive: { preserveEnd: true, labelSummary: true }` retains the final x-axis boundary and moves overflowing endpoint identities to a wrapped list. Explicit axis values and author choices remain authoritative. `chromeLayout: 'grow'` gives that content space. Tables may opt into `mobileLayout: 'priority'`; `cards` remains the default, with `scroll` also available.
- **Field metadata:** chart, table and map documents accept `fields`, keyed by field name. Each entry may declare `type` (`text`, `number`, `date`), `role: 'identity'`, `label`, `unit`, `measure`, `provenance`, `representation` (`number`, `percent`, `proportion`), `interval: { lower: 'lowerField', upper: 'upperField' }`, or `statusField`. Supplied intervals are never synthesized. Percent means 0–100 and proportion means 0–1.
- **Reader identity:** a unique authored `encoding.key` supports observation persistence. Missing/duplicate keys clear observation pins on data replacement. Aggregated observations are groups, not raw rows, and the reference reader clears them on data/grouping revision. The `createReader` controller is independent from editor selection and uses `ChartInstance.sampleLayout()` for supported moving geometry. Call `reader.update(spec)` after `chart.update(spec)`; it reconciles visibility against the compiled destination marks even when the source data array is unchanged. An explicit `visibleKeys` override remains supported. Layered charts retain per-layer hover/crosshair inspection, but reader pinning across layers is not yet supported: a `LayerSpec` clears pins with an announcement rather than guessing cross-layer identity.
-- **Design review:** `diagnoseChart(spec, layout)` returns structured checks with code, severity, checkability, path, viewport, explanation and origin. `CompileOptions.onDiagnostic` receives these checks. `recommendCompositions(data, question, roles)` requires explicit field roles and returns explained alternatives. `applySpecPatch` rejects stale snapshot preconditions; hosts should preview patches and retain undo. The reference editor uses the full authored document for patches even while previewing a filtered scene.
+- **Design review:** `diagnoseChart(spec, layout)` returns structured checks with code, severity, checkability, path, viewport, explanation and origin. `CompileOptions.onDiagnostic` receives these checks. `recommendCompositions(data, question, roles)` requires explicit field roles and returns explained alternatives. A diagnostic `fix` is a `SpecPatch` whose `changes` are `EditOp`s. `applySpecPatch(spec, patch)` rejects stale snapshot preconditions, applies the operations atomically through `applyEditTransaction`, and returns `{ spec, inverse }`; apply `inverse` to the patched document to undo. Hosts should preview patches before applying them. The reference editor uses the full authored document for patches even while previewing a filtered scene.
Host-neutral identity, comparison and explicit dataset-binding helpers are also exported from the engine. They provide foundations for future linked views, without implicitly declaring compatible measures or raw-row/aggregate relationships. See the [delivery record](design-evolution.md) for the advanced capabilities and release gates still pending.
@@ -2846,18 +2833,18 @@ Maps, tile maps, bar lists and Sankey support arrows for inspection and Enter/Sp
Table `rowKey` values must be unique to support identity across revisions. Numeric `1` and string `"1"` are distinct. Missing/duplicate keys emit `TABLE_ROW_KEY_AMBIGUOUS` and use revision-local identities. Treat compiled row IDs as opaque. CSV export includes all filtered and sorted rows, not just the current pagination page.
-### Compact callout limits and note sizing (8.9.1)
+### Compact callout limits and note sizing
`responsive.compactAnnotationLimit?: number | null` is an optional nonnegative
-integer limit below 400px outer width. Omitted/null means unlimited; zero demotes
-all eligible callouts. `responsive: false` callouts are exempt. Priority/source
+integer limit below 400px outer width. Omitted means one; null means unlimited,
+and zero demotes all eligible callouts. `responsive: false` callouts are exempt. Priority/source
order precedes coverage/collision thinning. Existing disabled-response and
short-height rules still apply. Facet panel width does not trigger this limit.
Read `layout.chrome.noteBandHeight` for note space and `layout.dimensions.height`
for the final rendered/exported height. `chromeLayout: 'grow'` also supports facets;
`chrome.grownHeight` reports engine-added height for auto-height consumers.
-See [annotation migration guidance](./migrating-v8.md#annotation-layout-opt-ins).
+See [annotation migration guidance](./migrating-v9.md#changed-annotation-and-figure-defaults).
---
@@ -3060,13 +3047,13 @@ const spec = {
{ os: "Other", share: 1 },
],
encoding: {
- y: { field: "share", type: "quantitative" },
+ theta: { field: "share", type: "quantitative" },
color: { field: "os", type: "nominal" },
},
};
```
-The `y` channel maps to slice values, `color` maps to slice categories. No `x` encoding is needed.
+The `theta` channel maps to slice values, `color` maps to slice categories. No `x` encoding is needed.
**Live examples**: [Basic pie](https://tryopendata.github.io/openchart/?story=charts--pie-and-donut#pie-inline-labels) | [Small slice grouping](https://tryopendata.github.io/openchart/?story=charts--pie-and-donut#small-slice-grouping) | [Seven categories](https://tryopendata.github.io/openchart/?story=charts--pie-and-donut#many-categories)
@@ -3087,7 +3074,7 @@ const spec = {
{ category: "All Other", spending: 29 },
],
encoding: {
- y: { field: "spending", type: "quantitative" },
+ theta: { field: "spending", type: "quantitative" },
color: { field: "category", type: "nominal" },
},
};
@@ -3212,7 +3199,7 @@ const spec = {
```
-Options: `nodeWidth`, `nodePadding`, `nodeAlign` (`'justify'` | `'left'` | `'right'` | `'center'`), `linkStyle` (`'gradient'` | `'source'` | `'target'` | `'neutral'`), `encoding.value.format` for number formatting (top-level `valueFormat` is deprecated in v8).
+Options: `nodeWidth`, `nodePadding`, `nodeAlign` (`'justify'` | `'left'` | `'right'` | `'center'`), `linkStyle` (`'gradient'` | `'source'` | `'target'` | `'neutral'`), `encoding.value.format` for number formatting (top-level `valueFormat` is removed in v9).
For the full field reference, see [SankeySpec in spec-reference.md](spec-reference.md#sankeyspec).
@@ -3431,7 +3418,7 @@ const spec = {
};
```
-`theta` is the quantitative share value (the same part-to-whole channel arc marks use; it is an alias for `y`, so provide one of the two). `color` is the required category. Values normalize to the grid via largest-remainder rounding, so the cells always sum exactly even when the shares are fractional.
+`theta` is the quantitative share value (the same part-to-whole channel arc marks use; the old authored `y` alias is removed). `color` is the required category. Values normalize to the grid via largest-remainder rounding, so the cells always sum exactly even when the shares are fractional.
**Mark options:**
- `units` (default 100) -- total number of cells in the grid
@@ -3497,7 +3484,7 @@ const spec = {
};
```
-`theta` is the quantitative seat count (an alias for `y`; provide one of the two) and `color` is the required party. Order the data left to right by political group; the layout preserves that order across the arc. Party colors typically come from an explicit `color.scale.range` rather than the default palette.
+`theta` is the quantitative seat count (the old authored `y` alias is removed) and `color` is the required party. Order the data left to right by political group; the layout preserves that order across the arc. Party colors typically come from an explicit `color.scale.range` rather than the default palette.
**Mark options:**
- `shape` (`'hemicycle'`, the default and only shape)
@@ -4162,7 +4149,6 @@ All features are opt-in via top-level `TableSpec` properties.
| Striping | `striped: true` | `false` | Zebra rows. Hairlines only by default |
| Initial sort | `sort: { column, direction }` | first inline-bar column, desc | Sort applied before the user touches a header |
| Total row | `totalRow: true` | `false` | Sticky footer summing every quantitative column with a summable cell type (text/bar/heatmap). Set `columns[].total: false` to leave an individual column's total cell blank |
-| Compact mode | `compact: true` | `false` | **Deprecated.** Alias for `density: 'condensed'`; warns |
| Responsive | `responsive: true` | `true` | Table adapts to container width |
In controlled mode a `sort` of `null` means "the user cleared sorting" and
@@ -4209,6 +4195,15 @@ share whatever space is left. If the explicit widths on their own would
exceed the container, they're scaled down proportionally (floored at 60px)
and the engine emits a `TABLE_WIDTH_OVERFLOW` warning through `onWarn`.
+### Surface
+
+Tables paint `theme.colors.background`, which is transparent by default, so a
+table sits on the host surface like a chart does. The header, totals row,
+search input and pager buttons are transparent too; the opt-in sticky first
+column keeps the mode's `--oc-bg` so scrolled cells don't show through. Inline
+bar fills render at 40% opacity in dark mode (18% in light). Set
+`theme.colors.background` for an opaque table.
+
### Full-featured example
```ts
@@ -4260,19 +4255,15 @@ For controlled component patterns and detailed event handling, see the [integrat
-# Migration Guide
-
-## Theming Design Tokens
-
-This release widens `ThemeConfig` to expose every design-decision field and unifies CSS custom properties with the JS theme engine. All changes are backward-compatible. Existing theme configs continue to work without modification.
+# Theming Design Tokens
-### What changed
+`ThemeConfig` controls colors, typography, spacing, rules, and component surfaces.
+Defaults are shared by the JavaScript theme and generated CSS tokens. v9
+consolidates authored deep merging and leaves omitted CSS tokens available to
+host styles. For removed forms and legacy styling, see [Migrating to v9](migrating-v9.md).
-**Before:** `ThemeConfig` covered colors, font family/sizes, 4 spacing fields, border radius, and chrome color (string only). Font weights, chrome typography, semantic colors, and additional spacing fields were hardcoded in `DEFAULT_THEME` with no override path. CSS custom properties in `tokens.css`/`dark.css` were hand-maintained separately from the JS theme.
+### ThemeConfig fields
-**After:** `ThemeConfig` covers everything. CSS custom properties are generated from the resolved JS theme at mount time. The static CSS files serve as SSR/pre-mount fallback defaults.
-
-### New ThemeConfig fields
```ts
const theme: ThemeConfig = {
@@ -4314,8 +4305,8 @@ const theme: ThemeConfig = {
subtitle: { fontWeight: 400, lineHeight: 1.4 },
eyebrow: { color: '#e3120b', fontWeight: 700 },
- // Plain strings still work (color-only, backward-compatible)
- source: '#666',
+ // Color-only overrides use the same object form.
+ source: { color: '#666' },
},
// New: series color assignment strategy
@@ -4355,11 +4346,10 @@ The neutral grays are surface-aware: on light backgrounds the second series gets
### Named presets
-Five presets exported from `@opendata-ai/openchart-core`:
+Four primary presets exported from `@opendata-ai/openchart-core`:
| Preset | Feel |
|--------|------|
-| `editorial` | Current default (empty config, zero drift) |
| `essay` | Serif titles, warm background, generous spacing |
| `wire` | Monospace, dense, tight chrome, dashboard feel |
| `broadsheet` | Newspaper editorial: warm paper, red masthead `rule` and eyebrow, 24/700 title, square corners |
@@ -4409,11 +4399,11 @@ fixed zinc ladder. A warm or cool custom theme gets warm or cool grays for
free instead of zinc showing up uninvited beside the palette. The ramp lives
on `ResolvedTheme.colors.neutral` (`100`/`200`/`300`/`400`/`600`/`800`, plus
`secondary` = `800`, `faint` = `300`, `border` = `100`, plus `surface`) and is
-computed once in `resolveTheme`/`adaptTheme`; mounts stamp the CSS tokens from
-it. `surface` is the opaque color the theme paints on — the background itself
+computed once in `resolveTheme`/`adaptTheme`. Mounts stamp derived tokens when
+the relevant theme colors are authored; omitted tokens use the CSS cascade. `surface` is the opaque color the theme paints on — the background itself
when it is opaque, otherwise the mode's `--oc-bg` token — and is the single
source for knockout rings, stacked-segment seams, table cell fills, graph node
-rings and the stamped `--oc-bg`. When the
+rings, annotation marker/plate fills, and authored background tokens. When the
background is transparent (the default) the ramp falls back to the static
token defaults, which is why the default theme and the generated `tokens.css`
never disagree.
@@ -4431,9 +4421,9 @@ never disagree.
| `--oc-map-hover-dim` | 0.75 | map-local override; dimming 3000 county paths at 0.3 reads as a blackout, so maps use a lighter dim and only on legend-bin hover, never on feature hover |
| `--oc-positive-tint` / `--oc-negative-tint` | 10% color-mix toward `--oc-bg` | delta chip backgrounds |
-`--oc-border-radius` (8px) drives containers and tooltips. In v8, mounts stamp
-resolved theme tokens inline, so ordinary stylesheet overrides of these tokens
-do not win. Set `theme.borderRadius` to change this value. Mark geometry uses the fixed `--oc-radius-sm`
+`--oc-border-radius` (8px) drives containers and tooltips. In v9, CSS overrides
+work when `theme.borderRadius` is omitted. An authored `theme.borderRadius`
+takes precedence. Mark geometry uses the fixed `--oc-radius-sm`
regardless, so a theme with `borderRadius: 0` still draws a 2px bar-end
radius.
@@ -4442,7 +4432,7 @@ radius.
`token-theme-parity` test asserts this, along with the four weight tokens,
`--oc-gridline`, `--oc-axis`, `--oc-border-radius`, `colors.neutral`, and
`--oc-animation-stagger`, so the JS theme and the generated CSS tokens can't
-drift apart the way they did before this release.
+drift apart.
### The editorial rule
@@ -4462,16 +4452,32 @@ as `chrome.*.color` and needs no dark-mode branch. `computeChrome` reserves
### CSS custom properties
-`--oc-*` custom properties are now stamped on each `.oc-root` container from the resolved JS theme at mount time. This means:
-
-- **Two charts with different themes on the same page** each get correct `--oc-*` values (per-container scoping).
-- **`tokens.css` and `dark.css` are fallback defaults**, not the source of truth. They still work for SSR, static HTML, and pre-mount styling.
-- **If you were reading `--oc-*` values from CSS** to style surrounding UI, those values now reflect the actual resolved theme rather than the hardcoded defaults. This is almost certainly what you wanted, but worth noting.
-
-No action required. If you import `tokens.css`/`dark.css`, keep doing so. The JS-stamped properties take precedence via inline style specificity.
-
-
-### Annotation tokens (8.9.1)
+Defaults remain in `tokens.css` and `dark.css`. The shared renderer helper stamps
+only authored theme leaves and values derived from them. A host stylesheet can
+override omitted tokens, including `--oc-border-radius`; an authored theme value
+wins via its inline style. Removing a theme override restores the host value.
+SVG and raster exports retain the live CSS token values before leaving the page.
+
+Theme layers deep-merge in this order: spec theme, provider theme, component prop
+(or mount/compile option). For layered charts, chrome, axes and legend use the
+layer theme (or the first leaf's when the layer sets none), and each leaf's marks
+use its own theme merged over the layer theme.
+Objects merge by leaf; arrays and `{ light, dark }` pairs replace atomically.
+`colors: string[]` remains shorthand for `colors.categorical`. `null` replaces:
+use `rule: null` to remove a preset rule. Partial rules inherit unspecified
+preset values; without a preset, width is 40px, thickness is 3px, and color is
+theme text ink. Authored `chrome..fontSize` overrides the general
+`fonts.sizes` scale. Chrome color shorthand is now `{ color: '...' }`.
+
+The default small/axis tick/source/byline/footer size is 12px; chrome-to-chart
+spacing is 16px and chart-to-footer spacing is 12px. The background remains
+transparent. The empty `editorial` preset was removed (omit `theme` instead).
+`clarity` is deprecated and retains only its background, axis, and eyebrow
+colors. Use `ThemeConfig` for input and `ResolvedTheme` for output; the `Theme`
+alias was removed.
+
+
+### Annotation tokens
`theme.colors` accepts `annotationLabel`, `annotationMuted`, `annotationLeader`,
`annotationRule`, `annotationRangeFill`, `annotationMarkerFill`, and
@@ -4480,4 +4486,6 @@ are `--oc-annotation-label`, `--oc-annotation-muted`, `--oc-annotation-leader`,
`--oc-annotation-rule`, `--oc-annotation-range-fill`, `--oc-annotation-marker-fill`,
and `--oc-annotation-plate`. Explicit annotation colors override the tokens.
Arrow connectors match label ink. Existing `annotationText`/`annotationFill`
-uses are unchanged. Omitted annotation tokens preserve current 8.x output.
+uses are unchanged. Default rule ink is #888888 in light mode and #9ca3af in
+dark mode; range fill is #f0c040. Marker and plate fills follow the resolved
+neutral surface unless explicitly authored.
diff --git a/llms.txt b/llms.txt
index 238297ba..83aeb289 100644
--- a/llms.txt
+++ b/llms.txt
@@ -24,7 +24,7 @@ The engine validates the spec, compiles layout and scales, and renders SVG (char
## VizSpec
-VizSpec is a union of six spec kinds, discriminated differently per kind:
+VizSpec is a union of these spec kinds, discriminated differently per kind:
- **ChartSpec**: has a `mark` field (a mark string or MarkDef object). No `type`.
- **LayerSpec**: has a `layer` array (overlay multiple charts on shared scales).
@@ -33,6 +33,7 @@ VizSpec is a union of six spec kinds, discriminated differently per kind:
- **SankeySpec**: `type: "sankey"`.
- **TileMapSpec**: `type: "tilemap"`.
- **BarListSpec**: `type: "barlist"`.
+- **GeoMapSpec**: `type: "map"`.
There is no `type: "line"` grammar. Charts use `mark`, following Vega-Lite. `mark: "line"`, not `type: "line"`.
@@ -56,7 +57,6 @@ There is no `type: "line"` grammar. Charts use `mark`, following Vega-Lite. `mar
labels?: boolean | LabelConfig, // direct data labels
legend?: LegendConfig, // { position, show, columns, ... }
theme?: ThemeConfig, // colors, fonts, spacing overrides
- darkMode?: "auto" | "force" | "off",
a11y?: { description?: string, hidden?: boolean },
description?: string, // sugar -> a11y.description (alt text)
width?: number, height?: number,
@@ -137,7 +137,7 @@ A bin plus a count aggregate is a histogram. Take both bin edges (`as: ["bin_x",
- `{ type: "text", x, y, text, subtitle?, connector?, dot?, anchor? }` - callout at a data coordinate.
- `{ type: "range", x1?, x2?, y1?, y2?, label? }` - highlighted band.
-- `{ type: "refline" | "rule", x?, y?, style?, label? }` - reference line at a value.
+- `{ type: "refline", x?, y?, style?, label?, labelPlacement? }` - reference line at a value.
## Chrome
@@ -151,7 +151,7 @@ Each accepts a string or `{ text, style?, offset? }`.
## Theme
-`theme` overrides design tokens: `{ colors?, fonts?, spacing?, borderRadius?, chrome? }`. `colors` accepts a categorical array or an object (`categorical`, `sequential`, `diverging`, `background`, `text`, `gridline`, `positive`, `negative`, ...). `darkMode` ("auto" | "force" | "off") controls automatic dark adaptation.
+`theme` overrides design tokens: `{ colors?, fonts?, spacing?, borderRadius?, chrome? }`. `colors` accepts a categorical array or an object (`categorical`, `sequential`, `diverging`, `background`, `text`, `gridline`, `positive`, `negative`, ...). Dark mode is not a spec field: pass `darkMode` ("auto" | "force" | "off") as a mount option or component prop.
## TableSpec
@@ -164,9 +164,8 @@ Each accepts a string or `{ text, style?, offset? }`.
search?: boolean,
pagination?: boolean | { pageSize: number },
stickyFirstColumn?: boolean,
- compact?: boolean,
+ density?: "condensed" | "regular" | "relaxed",
theme?: ThemeConfig,
- darkMode?: "auto" | "force" | "off",
}
```
@@ -180,10 +179,29 @@ Each accepts a string or `{ text, style?, offset? }`.
encoding?: { nodeColor?, nodeSize?, edgeColor?, edgeWidth?, nodeLabel? },
layout?: { type: "force" | "radial" | "hierarchical", clustering?, chargeStrength?, linkDistance? },
chrome?: Chrome,
- darkMode?: "auto" | "force" | "off",
}
```
+## GeoMapSpec
+
+```typescript
+{
+ type: "map",
+ geo: { features: topology, projection?: "albersUsa" | "mercator" | "equalEarth" | "identity", zoom?: boolean },
+ data: rows,
+ encoding: { key?, color?, tooltip?, text?, textPriority? },
+ points?: { data, longitude, latitude, key?, size?, color?, tooltip?, text?, textPriority? },
+ callouts?: [{ id, anchor, text, subtitle?, priority?, minZoom?, maxZoom?, offset?, maxWidth? }],
+ chrome?, legend?, theme?, fields?,
+}
+```
+
+Use `createGeoMap(container, spec)` or the `GeoMap` framework component. Labels are opt-in: `text: { field: "placeName" }`; quantitative `textPriority` orders bounded placement. Point labels and point-anchored callouts require a unique `points.key`.
+
+Callout anchors are `{ kind: "feature", id }`, `{ kind: "point", id }`, or `{ kind: "coordinate", space: "geographic" | "source", x, y }`. Feature IDs refer to topology features; point IDs refer to the point key. Geographic anchors use longitude/latitude and require a geographic projection; source anchors use the preprojected identity map's coordinate system. Do not interchange these spaces. Callout IDs must be unique.
+
+Labels retain constant text size during camera movement. Decluttered text remains in the accessible inventory; callouts that do not fit use reserved numbered notes. Complete authored inventory is included in standalone SVG descriptions. A visible place-list disclosure is a runtime `labelInventory: { container: hostElement }` option, independent of any reader panel. The host owns its position. Omission does not add visible HTML or labels, and map callouts are not Cartesian editable annotations.
+
## SankeySpec, TileMapSpec, BarListSpec
- `{ type: "sankey", data, encoding: { source, target, value }, ... }` - flow diagram.
@@ -196,7 +214,7 @@ Every chart ships a screen-reader description, a hidden data table, and keyboard
## Validation and repair
-Validate a spec before rendering with `validateSpec(spec)` from `@opendata-ai/openchart-engine`. It returns `{ valid, errors, normalized }`. Each error carries a machine-readable `code`, the offending `path`, and a repair-friendly `suggestion`. Field references are checked against the columns in the provided `data`; a misspelled field name suggests the nearest actual column ("did you mean").
+Validate a spec before rendering with `validateSpec(spec)` from `@opendata-ai/openchart-engine`. It returns `{ valid, errors, normalized }`. Each error carries a machine-readable `code`, the offending `path` as a root-relative JSON Pointer (e.g. `/encoding/y/field`, `/layer/1/data/0/value`), and a repair-friendly `suggestion`. Field references are checked against the columns in the provided `data`; a misspelled field name suggests the nearest actual column ("did you mean").
## JSON Schema
diff --git a/package.json b/package.json
index fa387292..bd1bedc6 100644
--- a/package.json
+++ b/package.json
@@ -39,6 +39,7 @@
"test:perf": "playwright test --project=perf",
"prepare": "husky",
"capture:design": "node scripts/capture-design-baseline.mjs",
+ "test:parity:capture": "node scripts/capture-v8-parity.mjs",
"check:boundaries": "node scripts/check-product-boundary.mjs",
"check:library-bundle": "node scripts/check-library-bundle.mjs"
},
diff --git a/packages/core/README.md b/packages/core/README.md
index 6e0e7313..145511c0 100644
--- a/packages/core/README.md
+++ b/packages/core/README.md
@@ -22,7 +22,7 @@ const columns = columnChart(data, 'month', 'revenue');
const bars = barChart(data, 'category', 'count');
```
-All chart builders accept an optional `options` argument for color encoding, annotations, chrome (title/subtitle/source), theme overrides, and dark mode.
+All chart builders accept an optional `options` argument for color encoding, annotations, chrome (title/subtitle/source), theme overrides, and responsive behavior. Dark mode is a mount/component option, not a spec field.
```typescript
const spec = lineChart(data, 'date', 'value', {
@@ -122,7 +122,8 @@ function describe(spec: VizSpec): string {
Beyond spec builders, this package provides:
-- **Theme system** - `DEFAULT_THEME`, `resolveTheme()`, `adaptTheme()` for dark mode adaptation
+- **Theme system** - `DEFAULT_THEME`, `resolveTheme()`, `mergeThemeConfig()` for deep-merging authored theme layers, `adaptTheme()` for dark mode adaptation
+- **Editing** - `applyEditTransaction()` applies the `EditTransaction` emitted by `onEdit` and returns the new spec plus its inverse
- **Color palettes** - `CATEGORICAL_PALETTE`, `SEQUENTIAL_PALETTES`, `DIVERGING_PALETTES`
- **Accessibility** - `generateAltText()`, `generateAriaLabels()`, contrast ratio utilities
- **Formatting** - `formatNumber()`, `formatDate()`, `abbreviateNumber()`
diff --git a/packages/core/schema/chart.schema.json b/packages/core/schema/chart.schema.json
index 944f3498..bda54a18 100644
--- a/packages/core/schema/chart.schema.json
+++ b/packages/core/schema/chart.schema.json
@@ -31,56 +31,6 @@
"type": "object",
"additionalProperties": false,
"properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
- "type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
"innerRadius": {
"type": "number",
"description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
@@ -120,17 +70,9 @@
],
"description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
},
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
+ "type": {
+ "type": "string",
+ "const": "arc"
},
"size": {
"type": "number",
@@ -148,26 +90,6 @@
"type": "number",
"description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
"fill": {
"anyOf": [
{
@@ -198,47 +120,6 @@
"type": "boolean",
"description": "Clip marks to the chart area."
},
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
"fillPattern": {
"type": "string",
"enum": [
@@ -246,99 +127,6 @@
"none"
],
"description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
}
},
"required": [
@@ -377,10 +165,6 @@
"type": "object",
"additionalProperties": false,
"properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
"point": {
"anyOf": [
{
@@ -419,52 +203,108 @@
],
"description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
},
- "orient": {
+ "type": {
"type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
+ "const": "line"
},
- "innerRadius": {
+ "size": {
"type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
+ "description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
},
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
+ "filled": {
+ "type": "boolean",
+ "description": "Whether the mark is filled (vs stroked only)."
},
- "startAngle": {
+ "opacity": {
"type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
+ "description": "Default opacity (0-1)."
},
- "endAngle": {
+ "fillOpacity": {
"type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
+ "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "centerLabel": {
+ "fill": {
"anyOf": [
{
"type": "string"
},
{
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
+ "$ref": "#/definitions/GradientDef"
}
],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
+ "description": "Default fill color or gradient."
+ },
+ "stroke": {
+ "type": "string",
+ "description": "Default stroke color."
+ },
+ "strokeWidth": {
+ "type": "number",
+ "description": "Default stroke width."
+ },
+ "tooltip": {
+ "type": [
+ "boolean",
+ "null"
+ ],
+ "description": "Tooltip behavior. null disables tooltips."
+ },
+ "clip": {
+ "type": "boolean",
+ "description": "Clip marks to the chart area."
+ },
+ "fillPattern": {
+ "type": "string",
+ "enum": [
+ "auto",
+ "none"
+ ],
+ "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
+ }
+ },
+ "required": [
+ "type"
+ ]
+ }
+ ]
+ },
+ "encoding": {
+ "$ref": "#/definitions/LineEncoding%3CDataRow%3E"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/definitions/ChartSpecBase"
+ },
+ {
+ "type": "object",
+ "required": [
+ "data",
+ "encoding",
+ "mark"
+ ],
+ "properties": {
+ "mark": {
+ "anyOf": [
+ {
+ "type": "string",
+ "const": "bar"
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "orient": {
+ "type": "string",
+ "enum": [
+ "horizontal",
+ "vertical"
+ ],
+ "description": "Explicit orientation override for bar marks."
},
"cornerRadius": {
"anyOf": [
@@ -478,6 +318,10 @@
],
"description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
},
+ "type": {
+ "type": "string",
+ "const": "bar"
+ },
"size": {
"type": "number",
"description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
@@ -494,26 +338,6 @@
"type": "number",
"description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
"fill": {
"anyOf": [
{
@@ -544,47 +368,6 @@
"type": "boolean",
"description": "Clip marks to the chart area."
},
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
"fillPattern": {
"type": "string",
"enum": [
@@ -592,99 +375,6 @@
"none"
],
"description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
}
},
"required": [
@@ -694,7 +384,7 @@
]
},
"encoding": {
- "$ref": "#/definitions/LineEncoding%3CDataRow%3E"
+ "$ref": "#/definitions/BarEncoding%3CDataRow%3E"
}
}
}
@@ -717,16 +407,12 @@
"anyOf": [
{
"type": "string",
- "const": "bar"
+ "const": "area"
},
{
"type": "object",
"additionalProperties": false,
"properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
"point": {
"anyOf": [
{
@@ -765,72 +451,17 @@
],
"description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
},
- "orient": {
+ "type": {
"type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
+ "const": "area"
},
- "innerRadius": {
+ "size": {
"type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
+ "description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
},
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
- },
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
- },
- "size": {
- "type": "number",
- "description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
- },
- "filled": {
- "type": "boolean",
- "description": "Whether the mark is filled (vs stroked only)."
+ "filled": {
+ "type": "boolean",
+ "description": "Whether the mark is filled (vs stroked only)."
},
"opacity": {
"type": "number",
@@ -840,26 +471,6 @@
"type": "number",
"description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
"fill": {
"anyOf": [
{
@@ -890,6 +501,51 @@
"type": "boolean",
"description": "Clip marks to the chart area."
},
+ "fillPattern": {
+ "type": "string",
+ "enum": [
+ "auto",
+ "none"
+ ],
+ "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
+ }
+ },
+ "required": [
+ "type"
+ ]
+ }
+ ]
+ },
+ "encoding": {
+ "$ref": "#/definitions/AreaEncoding%3CDataRow%3E"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/definitions/ChartSpecBase"
+ },
+ {
+ "type": "object",
+ "required": [
+ "data",
+ "encoding",
+ "mark"
+ ],
+ "properties": {
+ "mark": {
+ "anyOf": [
+ {
+ "type": "string",
+ "const": "point"
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
"trendline": {
"anyOf": [
{
@@ -901,136 +557,63 @@
],
"description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
},
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
+ "type": {
"type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
+ "const": "point"
},
- "fontSize": {
+ "size": {
"type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
- "fillPattern": {
- "type": "string",
- "enum": [
- "auto",
- "none"
- ],
- "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
+ "description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
},
- "colorByDirection": {
+ "filled": {
"type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
+ "description": "Whether the mark is filled (vs stroked only)."
},
- "units": {
+ "opacity": {
"type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
+ "description": "Default opacity (0-1)."
},
- "columns": {
+ "fillOpacity": {
"type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
+ "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "other": {
+ "fill": {
"anyOf": [
{
- "type": "number"
+ "type": "string"
},
{
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
+ "$ref": "#/definitions/GradientDef"
}
],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
+ "description": "Default fill color or gradient."
},
- "weekStart": {
+ "stroke": {
"type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
+ "description": "Default stroke color."
},
- "cellRadius": {
+ "strokeWidth": {
"type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
+ "description": "Default stroke width."
},
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
+ "tooltip": {
+ "type": [
+ "boolean",
+ "null"
],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
+ "description": "Tooltip behavior. null disables tooltips."
},
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
+ "clip": {
+ "type": "boolean",
+ "description": "Clip marks to the chart area."
+ },
+ "fillPattern": {
+ "type": "string",
+ "enum": [
+ "auto",
+ "none"
],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
+ "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
}
},
"required": [
@@ -1040,7 +623,7 @@
]
},
"encoding": {
- "$ref": "#/definitions/BarEncoding%3CDataRow%3E"
+ "$ref": "#/definitions/PointEncoding%3CDataRow%3E"
}
}
}
@@ -1063,112 +646,15 @@
"anyOf": [
{
"type": "string",
- "const": "area"
+ "const": "circle"
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
"type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
- },
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
+ "const": "circle"
},
"size": {
"type": "number",
@@ -1186,26 +672,6 @@
"type": "number",
"description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
"fill": {
"anyOf": [
{
@@ -1236,47 +702,6 @@
"type": "boolean",
"description": "Clip marks to the chart area."
},
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
"fillPattern": {
"type": "string",
"enum": [
@@ -1284,99 +709,6 @@
"none"
],
"description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
}
},
"required": [
@@ -1386,7 +718,7 @@
]
},
"encoding": {
- "$ref": "#/definitions/AreaEncoding%3CDataRow%3E"
+ "$ref": "#/definitions/CircleEncoding%3CDataRow%3E"
}
}
}
@@ -1409,112 +741,15 @@
"anyOf": [
{
"type": "string",
- "const": "point"
+ "const": "lollipop"
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
"type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
- },
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
+ "const": "lollipop"
},
"size": {
"type": "number",
@@ -1532,26 +767,6 @@
"type": "number",
"description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
"fill": {
"anyOf": [
{
@@ -1582,47 +797,6 @@
"type": "boolean",
"description": "Clip marks to the chart area."
},
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
"fillPattern": {
"type": "string",
"enum": [
@@ -1630,99 +804,6 @@
"none"
],
"description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
}
},
"required": [
@@ -1732,7 +813,7 @@
]
},
"encoding": {
- "$ref": "#/definitions/PointEncoding%3CDataRow%3E"
+ "$ref": "#/definitions/LollipopEncoding%3CDataRow%3E"
}
}
}
@@ -1755,112 +836,15 @@
"anyOf": [
{
"type": "string",
- "const": "circle"
+ "const": "beeswarm"
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
- "type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
- },
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
+ "type": "string",
+ "const": "beeswarm"
},
"size": {
"type": "number",
@@ -1878,26 +862,6 @@
"type": "number",
"description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
"fill": {
"anyOf": [
{
@@ -1928,47 +892,6 @@
"type": "boolean",
"description": "Clip marks to the chart area."
},
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
"fillPattern": {
"type": "string",
"enum": [
@@ -1976,99 +899,6 @@
"none"
],
"description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
}
},
"required": [
@@ -2078,7 +908,7 @@
]
},
"encoding": {
- "$ref": "#/definitions/CircleEncoding%3CDataRow%3E"
+ "$ref": "#/definitions/BeeswarmEncoding%3CDataRow%3E"
}
}
}
@@ -2101,2880 +931,28 @@
"anyOf": [
{
"type": "string",
- "const": "lollipop"
+ "const": "range"
},
{
"type": "object",
"additionalProperties": false,
"properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
- "type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
- },
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
- },
- "size": {
- "type": "number",
- "description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
- },
- "filled": {
- "type": "boolean",
- "description": "Whether the mark is filled (vs stroked only)."
- },
- "opacity": {
- "type": "number",
- "description": "Default opacity (0-1)."
- },
- "fillOpacity": {
- "type": "number",
- "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
- },
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
- "fill": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "$ref": "#/definitions/GradientDef"
- }
- ],
- "description": "Default fill color or gradient."
- },
- "stroke": {
- "type": "string",
- "description": "Default stroke color."
- },
- "strokeWidth": {
- "type": "number",
- "description": "Default stroke width."
- },
- "tooltip": {
- "type": [
- "boolean",
- "null"
- ],
- "description": "Tooltip behavior. null disables tooltips."
- },
- "clip": {
- "type": "boolean",
- "description": "Clip marks to the chart area."
- },
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
- "fillPattern": {
- "type": "string",
- "enum": [
- "auto",
- "none"
- ],
- "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
- }
- },
- "required": [
- "type"
- ]
- }
- ]
- },
- "encoding": {
- "$ref": "#/definitions/LollipopEncoding%3CDataRow%3E"
- }
- }
- }
- ]
- },
- {
- "allOf": [
- {
- "$ref": "#/definitions/ChartSpecBase"
- },
- {
- "type": "object",
- "required": [
- "data",
- "encoding",
- "mark"
- ],
- "properties": {
- "mark": {
- "anyOf": [
- {
- "type": "string",
- "const": "beeswarm"
- },
- {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
- "type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
- },
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
- },
- "size": {
- "type": "number",
- "description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
- },
- "filled": {
- "type": "boolean",
- "description": "Whether the mark is filled (vs stroked only)."
- },
- "opacity": {
- "type": "number",
- "description": "Default opacity (0-1)."
- },
- "fillOpacity": {
- "type": "number",
- "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
- },
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
- "fill": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "$ref": "#/definitions/GradientDef"
- }
- ],
- "description": "Default fill color or gradient."
- },
- "stroke": {
- "type": "string",
- "description": "Default stroke color."
- },
- "strokeWidth": {
- "type": "number",
- "description": "Default stroke width."
- },
- "tooltip": {
- "type": [
- "boolean",
- "null"
- ],
- "description": "Tooltip behavior. null disables tooltips."
- },
- "clip": {
- "type": "boolean",
- "description": "Clip marks to the chart area."
- },
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
- "fillPattern": {
- "type": "string",
- "enum": [
- "auto",
- "none"
- ],
- "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
- }
- },
- "required": [
- "type"
- ]
- }
- ]
- },
- "encoding": {
- "$ref": "#/definitions/BeeswarmEncoding%3CDataRow%3E"
- }
- }
- }
- ]
- },
- {
- "allOf": [
- {
- "$ref": "#/definitions/ChartSpecBase"
- },
- {
- "type": "object",
- "required": [
- "data",
- "encoding",
- "mark"
- ],
- "properties": {
- "mark": {
- "anyOf": [
- {
- "type": "string",
- "const": "range"
- },
- {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
- "type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
- },
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
- },
- "size": {
- "type": "number",
- "description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
- },
- "filled": {
- "type": "boolean",
- "description": "Whether the mark is filled (vs stroked only)."
- },
- "opacity": {
- "type": "number",
- "description": "Default opacity (0-1)."
- },
- "fillOpacity": {
- "type": "number",
- "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
- },
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
- "fill": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "$ref": "#/definitions/GradientDef"
- }
- ],
- "description": "Default fill color or gradient."
- },
- "stroke": {
- "type": "string",
- "description": "Default stroke color."
- },
- "strokeWidth": {
- "type": "number",
- "description": "Default stroke width."
- },
- "tooltip": {
- "type": [
- "boolean",
- "null"
- ],
- "description": "Tooltip behavior. null disables tooltips."
- },
- "clip": {
- "type": "boolean",
- "description": "Clip marks to the chart area."
- },
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
- "fillPattern": {
- "type": "string",
- "enum": [
- "auto",
- "none"
- ],
- "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
- }
- },
- "required": [
- "type"
- ]
- }
- ]
- },
- "encoding": {
- "$ref": "#/definitions/RangeEncoding%3CDataRow%3E"
- }
- }
- }
- ]
- },
- {
- "allOf": [
- {
- "$ref": "#/definitions/ChartSpecBase"
- },
- {
- "type": "object",
- "required": [
- "data",
- "encoding",
- "mark"
- ],
- "properties": {
- "mark": {
- "anyOf": [
- {
- "type": "string",
- "const": "waffle"
- },
- {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
- "type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
- },
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
- },
- "size": {
- "type": "number",
- "description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
- },
- "filled": {
- "type": "boolean",
- "description": "Whether the mark is filled (vs stroked only)."
- },
- "opacity": {
- "type": "number",
- "description": "Default opacity (0-1)."
- },
- "fillOpacity": {
- "type": "number",
- "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
- },
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
- "fill": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "$ref": "#/definitions/GradientDef"
- }
- ],
- "description": "Default fill color or gradient."
- },
- "stroke": {
- "type": "string",
- "description": "Default stroke color."
- },
- "strokeWidth": {
- "type": "number",
- "description": "Default stroke width."
- },
- "tooltip": {
- "type": [
- "boolean",
- "null"
- ],
- "description": "Tooltip behavior. null disables tooltips."
- },
- "clip": {
- "type": "boolean",
- "description": "Clip marks to the chart area."
- },
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
- "fillPattern": {
- "type": "string",
- "enum": [
- "auto",
- "none"
- ],
- "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
- }
- },
- "required": [
- "type"
- ]
- }
- ]
- },
- "encoding": {
- "$ref": "#/definitions/WaffleEncoding%3CDataRow%3E"
- }
- }
- }
- ]
- },
- {
- "allOf": [
- {
- "$ref": "#/definitions/ChartSpecBase"
- },
- {
- "type": "object",
- "required": [
- "data",
- "encoding",
- "mark"
- ],
- "properties": {
- "mark": {
- "anyOf": [
- {
- "type": "string",
- "const": "text"
- },
- {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
- "type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
- },
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
- },
- "size": {
- "type": "number",
- "description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
- },
- "filled": {
- "type": "boolean",
- "description": "Whether the mark is filled (vs stroked only)."
- },
- "opacity": {
- "type": "number",
- "description": "Default opacity (0-1)."
- },
- "fillOpacity": {
- "type": "number",
- "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
- },
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
- "fill": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "$ref": "#/definitions/GradientDef"
- }
- ],
- "description": "Default fill color or gradient."
- },
- "stroke": {
- "type": "string",
- "description": "Default stroke color."
- },
- "strokeWidth": {
- "type": "number",
- "description": "Default stroke width."
- },
- "tooltip": {
- "type": [
- "boolean",
- "null"
- ],
- "description": "Tooltip behavior. null disables tooltips."
- },
- "clip": {
- "type": "boolean",
- "description": "Clip marks to the chart area."
- },
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
- "fillPattern": {
- "type": "string",
- "enum": [
- "auto",
- "none"
- ],
- "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
- }
- },
- "required": [
- "type"
- ]
- }
- ]
- },
- "encoding": {
- "$ref": "#/definitions/TextEncoding%3CDataRow%3E"
- }
- }
- }
- ]
- },
- {
- "allOf": [
- {
- "$ref": "#/definitions/ChartSpecBase"
- },
- {
- "type": "object",
- "required": [
- "data",
- "encoding",
- "mark"
- ],
- "properties": {
- "mark": {
- "anyOf": [
- {
- "type": "string",
- "const": "tick"
- },
- {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
- "type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
- },
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
- },
- "size": {
- "type": "number",
- "description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
- },
- "filled": {
- "type": "boolean",
- "description": "Whether the mark is filled (vs stroked only)."
- },
- "opacity": {
- "type": "number",
- "description": "Default opacity (0-1)."
- },
- "fillOpacity": {
- "type": "number",
- "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
- },
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
- "fill": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "$ref": "#/definitions/GradientDef"
- }
- ],
- "description": "Default fill color or gradient."
- },
- "stroke": {
- "type": "string",
- "description": "Default stroke color."
- },
- "strokeWidth": {
- "type": "number",
- "description": "Default stroke width."
- },
- "tooltip": {
- "type": [
- "boolean",
- "null"
- ],
- "description": "Tooltip behavior. null disables tooltips."
- },
- "clip": {
- "type": "boolean",
- "description": "Clip marks to the chart area."
- },
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
- "fillPattern": {
- "type": "string",
- "enum": [
- "auto",
- "none"
- ],
- "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
- }
- },
- "required": [
- "type"
- ]
- }
- ]
- },
- "encoding": {
- "$ref": "#/definitions/TickEncoding%3CDataRow%3E"
- }
- }
- }
- ]
- },
- {
- "allOf": [
- {
- "$ref": "#/definitions/ChartSpecBase"
- },
- {
- "type": "object",
- "required": [
- "data",
- "encoding",
- "mark"
- ],
- "properties": {
- "mark": {
- "anyOf": [
- {
- "type": "string",
- "const": "rect"
- },
- {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
- "type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
- },
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
- },
- "size": {
- "type": "number",
- "description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
- },
- "filled": {
- "type": "boolean",
- "description": "Whether the mark is filled (vs stroked only)."
- },
- "opacity": {
- "type": "number",
- "description": "Default opacity (0-1)."
- },
- "fillOpacity": {
- "type": "number",
- "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
- },
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
- "fill": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "$ref": "#/definitions/GradientDef"
- }
- ],
- "description": "Default fill color or gradient."
- },
- "stroke": {
- "type": "string",
- "description": "Default stroke color."
- },
- "strokeWidth": {
- "type": "number",
- "description": "Default stroke width."
- },
- "tooltip": {
- "type": [
- "boolean",
- "null"
- ],
- "description": "Tooltip behavior. null disables tooltips."
- },
- "clip": {
- "type": "boolean",
- "description": "Clip marks to the chart area."
- },
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
- "fillPattern": {
- "type": "string",
- "enum": [
- "auto",
- "none"
- ],
- "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
- }
- },
- "required": [
- "type"
- ]
- }
- ]
- },
- "encoding": {
- "$ref": "#/definitions/RectEncoding%3CDataRow%3E"
- }
- }
- }
- ]
- },
- {
- "allOf": [
- {
- "$ref": "#/definitions/ChartSpecBase"
- },
- {
- "type": "object",
- "required": [
- "data",
- "encoding",
- "mark"
- ],
- "properties": {
- "mark": {
- "anyOf": [
- {
- "type": "string",
- "const": "rule"
- },
- {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
- "type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
- },
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
- },
- "size": {
- "type": "number",
- "description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
- },
- "filled": {
- "type": "boolean",
- "description": "Whether the mark is filled (vs stroked only)."
- },
- "opacity": {
- "type": "number",
- "description": "Default opacity (0-1)."
- },
- "fillOpacity": {
- "type": "number",
- "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
- },
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
- "fill": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "$ref": "#/definitions/GradientDef"
- }
- ],
- "description": "Default fill color or gradient."
- },
- "stroke": {
- "type": "string",
- "description": "Default stroke color."
- },
- "strokeWidth": {
- "type": "number",
- "description": "Default stroke width."
- },
- "tooltip": {
- "type": [
- "boolean",
- "null"
- ],
- "description": "Tooltip behavior. null disables tooltips."
- },
- "clip": {
- "type": "boolean",
- "description": "Clip marks to the chart area."
- },
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
- "fillPattern": {
- "type": "string",
- "enum": [
- "auto",
- "none"
- ],
- "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
- }
- },
- "required": [
- "type"
- ]
- }
- ]
- },
- "encoding": {
- "$ref": "#/definitions/Encoding%3CDataRow%3E"
- }
- }
- }
- ]
- },
- {
- "allOf": [
- {
- "$ref": "#/definitions/ChartSpecBase"
- },
- {
- "type": "object",
- "required": [
- "data",
- "encoding",
- "mark"
- ],
- "properties": {
- "mark": {
- "anyOf": [
- {
- "type": "string",
- "const": "calendar"
- },
- {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
+ "style": {
"type": "string",
"enum": [
- "horizontal",
- "vertical"
+ "dumbbell",
+ "arrow",
+ "bar"
],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
+ "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
},
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
+ "colorByDirection": {
+ "type": "boolean",
+ "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
},
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
+ "type": {
+ "type": "string",
+ "const": "range"
},
"size": {
"type": "number",
@@ -4992,26 +970,6 @@
"type": "number",
"description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
"fill": {
"anyOf": [
{
@@ -5042,47 +1000,6 @@
"type": "boolean",
"description": "Clip marks to the chart area."
},
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
"fillPattern": {
"type": "string",
"enum": [
@@ -5090,99 +1007,6 @@
"none"
],
"description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
}
},
"required": [
@@ -5192,7 +1016,7 @@
]
},
"encoding": {
- "$ref": "#/definitions/CalendarEncoding%3CDataRow%3E"
+ "$ref": "#/definitions/RangeEncoding%3CDataRow%3E"
}
}
}
@@ -5215,100 +1039,42 @@
"anyOf": [
{
"type": "string",
- "const": "parliament"
+ "const": "waffle"
},
{
"type": "object",
"additionalProperties": false,
"properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
- "type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
+ "units": {
"type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
+ "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
},
- "endAngle": {
+ "columns": {
"type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
+ "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
},
- "centerLabel": {
+ "other": {
"anyOf": [
{
- "type": "string"
+ "type": "number"
},
{
"type": "object",
"properties": {
- "text": {
- "type": "string"
+ "threshold": {
+ "type": "number"
},
- "subtitle": {
+ "label": {
"type": "string"
}
},
"required": [
- "text"
+ "threshold"
],
"additionalProperties": false
}
],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
+ "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
},
"cornerRadius": {
"anyOf": [
@@ -5322,6 +1088,10 @@
],
"description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
},
+ "type": {
+ "type": "string",
+ "const": "waffle"
+ },
"size": {
"type": "number",
"description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
@@ -5338,26 +1108,6 @@
"type": "number",
"description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
"fill": {
"anyOf": [
{
@@ -5388,17 +1138,51 @@
"type": "boolean",
"description": "Clip marks to the chart area."
},
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
+ "fillPattern": {
+ "type": "string",
+ "enum": [
+ "auto",
+ "none"
],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
+ "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
+ }
+ },
+ "required": [
+ "type"
+ ]
+ }
+ ]
+ },
+ "encoding": {
+ "$ref": "#/definitions/WaffleEncoding%3CDataRow%3E"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/definitions/ChartSpecBase"
+ },
+ {
+ "type": "object",
+ "required": [
+ "data",
+ "encoding",
+ "mark"
+ ],
+ "properties": {
+ "mark": {
+ "anyOf": [
+ {
+ "type": "string",
+ "const": "text"
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
"dx": {
"type": "number",
"description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
@@ -5429,106 +1213,63 @@
"type": "number",
"description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
},
- "fillPattern": {
+ "type": {
"type": "string",
- "enum": [
- "auto",
- "none"
- ],
- "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
+ "const": "text"
},
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
+ "size": {
+ "type": "number",
+ "description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
},
- "colorByDirection": {
+ "filled": {
"type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
+ "description": "Whether the mark is filled (vs stroked only)."
},
- "units": {
+ "opacity": {
"type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
+ "description": "Default opacity (0-1)."
},
- "columns": {
+ "fillOpacity": {
"type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
+ "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "other": {
+ "fill": {
"anyOf": [
{
- "type": "number"
+ "type": "string"
},
{
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
+ "$ref": "#/definitions/GradientDef"
}
],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
+ "description": "Default fill color or gradient."
},
- "shape": {
+ "stroke": {
"type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
+ "description": "Default stroke color."
},
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
+ "strokeWidth": {
+ "type": "number",
+ "description": "Default stroke width."
+ },
+ "tooltip": {
+ "type": [
+ "boolean",
+ "null"
],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
+ "description": "Tooltip behavior. null disables tooltips."
+ },
+ "clip": {
+ "type": "boolean",
+ "description": "Clip marks to the chart area."
+ },
+ "fillPattern": {
+ "type": "string",
+ "enum": [
+ "auto",
+ "none"
+ ],
+ "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
}
},
"required": [
@@ -5538,7 +1279,7 @@
]
},
"encoding": {
- "$ref": "#/definitions/ParliamentEncoding%3CDataRow%3E"
+ "$ref": "#/definitions/TextEncoding%3CDataRow%3E"
}
}
}
@@ -5561,101 +1302,107 @@
"anyOf": [
{
"type": "string",
- "const": "histogram"
+ "const": "tick"
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
"type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
+ "const": "tick"
},
- "innerRadius": {
+ "size": {
"type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
+ "description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
},
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
+ "filled": {
+ "type": "boolean",
+ "description": "Whether the mark is filled (vs stroked only)."
},
- "startAngle": {
+ "opacity": {
"type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
+ "description": "Default opacity (0-1)."
},
- "endAngle": {
+ "fillOpacity": {
"type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
+ "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "centerLabel": {
+ "fill": {
"anyOf": [
{
"type": "string"
},
{
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
+ "$ref": "#/definitions/GradientDef"
}
],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
+ "description": "Default fill color or gradient."
+ },
+ "stroke": {
+ "type": "string",
+ "description": "Default stroke color."
+ },
+ "strokeWidth": {
+ "type": "number",
+ "description": "Default stroke width."
+ },
+ "tooltip": {
+ "type": [
+ "boolean",
+ "null"
+ ],
+ "description": "Tooltip behavior. null disables tooltips."
+ },
+ "clip": {
+ "type": "boolean",
+ "description": "Clip marks to the chart area."
},
+ "fillPattern": {
+ "type": "string",
+ "enum": [
+ "auto",
+ "none"
+ ],
+ "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
+ }
+ },
+ "required": [
+ "type"
+ ]
+ }
+ ]
+ },
+ "encoding": {
+ "$ref": "#/definitions/TickEncoding%3CDataRow%3E"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/definitions/ChartSpecBase"
+ },
+ {
+ "type": "object",
+ "required": [
+ "data",
+ "encoding",
+ "mark"
+ ],
+ "properties": {
+ "mark": {
+ "anyOf": [
+ {
+ "type": "string",
+ "const": "rect"
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
"cornerRadius": {
"anyOf": [
{
@@ -5668,6 +1415,10 @@
],
"description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
},
+ "type": {
+ "type": "string",
+ "const": "rect"
+ },
"size": {
"type": "number",
"description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
@@ -5684,25 +1435,100 @@
"type": "number",
"description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "binCount": {
+ "fill": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "$ref": "#/definitions/GradientDef"
+ }
+ ],
+ "description": "Default fill color or gradient."
+ },
+ "stroke": {
+ "type": "string",
+ "description": "Default stroke color."
+ },
+ "strokeWidth": {
"type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
+ "description": "Default stroke width."
},
- "normalize": {
+ "tooltip": {
+ "type": [
+ "boolean",
+ "null"
+ ],
+ "description": "Tooltip behavior. null disables tooltips."
+ },
+ "clip": {
"type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
+ "description": "Clip marks to the chart area."
},
- "bandwidth": {
+ "fillPattern": {
+ "type": "string",
+ "enum": [
+ "auto",
+ "none"
+ ],
+ "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
+ }
+ },
+ "required": [
+ "type"
+ ]
+ }
+ ]
+ },
+ "encoding": {
+ "$ref": "#/definitions/RectEncoding%3CDataRow%3E"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/definitions/ChartSpecBase"
+ },
+ {
+ "type": "object",
+ "required": [
+ "data",
+ "encoding",
+ "mark"
+ ],
+ "properties": {
+ "mark": {
+ "anyOf": [
+ {
+ "type": "string",
+ "const": "rule"
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "rule"
+ },
+ "size": {
"type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
+ "description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
},
- "cumulative": {
+ "filled": {
"type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
+ "description": "Whether the mark is filled (vs stroked only)."
},
- "steps": {
+ "opacity": {
"type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
+ "description": "Default opacity (0-1)."
+ },
+ "fillOpacity": {
+ "type": "number",
+ "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
"fill": {
"anyOf": [
@@ -5734,111 +1560,158 @@
"type": "boolean",
"description": "Clip marks to the chart area."
},
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
+ "fillPattern": {
"type": "string",
"enum": [
- "left",
- "center",
- "right"
+ "auto",
+ "none"
],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
+ "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
+ }
+ },
+ "required": [
+ "type"
+ ]
+ }
+ ]
+ },
+ "encoding": {
+ "$ref": "#/definitions/Encoding%3CDataRow%3E"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/definitions/ChartSpecBase"
+ },
+ {
+ "type": "object",
+ "required": [
+ "data",
+ "encoding",
+ "mark"
+ ],
+ "properties": {
+ "mark": {
+ "anyOf": [
+ {
+ "type": "string",
+ "const": "calendar"
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "weekStart": {
"type": "string",
"enum": [
- "top",
- "middle",
- "bottom"
+ "monday",
+ "sunday"
],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
+ "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
},
- "fontSize": {
+ "cellRadius": {
"type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
+ "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
},
- "fillPattern": {
+ "type": {
"type": "string",
- "enum": [
- "auto",
- "none"
- ],
- "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
+ "const": "calendar"
},
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
+ "size": {
+ "type": "number",
+ "description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
},
- "colorByDirection": {
+ "filled": {
"type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
+ "description": "Whether the mark is filled (vs stroked only)."
},
- "units": {
+ "opacity": {
"type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
+ "description": "Default opacity (0-1)."
},
- "columns": {
+ "fillOpacity": {
"type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
+ "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "other": {
+ "fill": {
"anyOf": [
{
- "type": "number"
+ "type": "string"
},
{
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
+ "$ref": "#/definitions/GradientDef"
}
],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
+ "description": "Default fill color or gradient."
},
- "weekStart": {
+ "stroke": {
"type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
+ "description": "Default stroke color."
},
- "cellRadius": {
+ "strokeWidth": {
"type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
+ "description": "Default stroke width."
+ },
+ "tooltip": {
+ "type": [
+ "boolean",
+ "null"
+ ],
+ "description": "Tooltip behavior. null disables tooltips."
},
+ "clip": {
+ "type": "boolean",
+ "description": "Clip marks to the chart area."
+ },
+ "fillPattern": {
+ "type": "string",
+ "enum": [
+ "auto",
+ "none"
+ ],
+ "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
+ }
+ },
+ "required": [
+ "type"
+ ]
+ }
+ ]
+ },
+ "encoding": {
+ "$ref": "#/definitions/CalendarEncoding%3CDataRow%3E"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/definitions/ChartSpecBase"
+ },
+ {
+ "type": "object",
+ "required": [
+ "data",
+ "encoding",
+ "mark"
+ ],
+ "properties": {
+ "mark": {
+ "anyOf": [
+ {
+ "type": "string",
+ "const": "parliament"
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
"shape": {
"type": "string",
"const": "hemicycle",
@@ -5874,7 +1747,65 @@
"additionalProperties": false
}
],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
+ "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
+ },
+ "type": {
+ "type": "string",
+ "const": "parliament"
+ },
+ "size": {
+ "type": "number",
+ "description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
+ },
+ "filled": {
+ "type": "boolean",
+ "description": "Whether the mark is filled (vs stroked only)."
+ },
+ "opacity": {
+ "type": "number",
+ "description": "Default opacity (0-1)."
+ },
+ "fillOpacity": {
+ "type": "number",
+ "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
+ },
+ "fill": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "$ref": "#/definitions/GradientDef"
+ }
+ ],
+ "description": "Default fill color or gradient."
+ },
+ "stroke": {
+ "type": "string",
+ "description": "Default stroke color."
+ },
+ "strokeWidth": {
+ "type": "number",
+ "description": "Default stroke width."
+ },
+ "tooltip": {
+ "type": [
+ "boolean",
+ "null"
+ ],
+ "description": "Tooltip behavior. null disables tooltips."
+ },
+ "clip": {
+ "type": "boolean",
+ "description": "Clip marks to the chart area."
+ },
+ "fillPattern": {
+ "type": "string",
+ "enum": [
+ "auto",
+ "none"
+ ],
+ "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
}
},
"required": [
@@ -5884,7 +1815,7 @@
]
},
"encoding": {
- "$ref": "#/definitions/HistogramEncoding%3CDataRow%3E"
+ "$ref": "#/definitions/ParliamentEncoding%3CDataRow%3E"
}
}
}
@@ -5907,54 +1838,12 @@
"anyOf": [
{
"type": "string",
- "const": "density"
+ "const": "histogram"
},
{
"type": "object",
"additionalProperties": false,
"properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
"orient": {
"type": "string",
"enum": [
@@ -5963,45 +1852,6 @@
],
"description": "Explicit orientation override for bar marks."
},
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
- },
"cornerRadius": {
"anyOf": [
{
@@ -6014,6 +1864,18 @@
],
"description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
},
+ "binCount": {
+ "type": "number",
+ "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
+ },
+ "normalize": {
+ "type": "boolean",
+ "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
+ },
+ "type": {
+ "type": "string",
+ "const": "histogram"
+ },
"size": {
"type": "number",
"description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
@@ -6030,26 +1892,6 @@
"type": "number",
"description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
"fill": {
"anyOf": [
{
@@ -6080,147 +1922,158 @@
"type": "boolean",
"description": "Clip marks to the chart area."
},
- "trendline": {
+ "fillPattern": {
+ "type": "string",
+ "enum": [
+ "auto",
+ "none"
+ ],
+ "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
+ }
+ },
+ "required": [
+ "type"
+ ]
+ }
+ ]
+ },
+ "encoding": {
+ "$ref": "#/definitions/HistogramEncoding%3CDataRow%3E"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/definitions/ChartSpecBase"
+ },
+ {
+ "type": "object",
+ "required": [
+ "data",
+ "encoding",
+ "mark"
+ ],
+ "properties": {
+ "mark": {
+ "anyOf": [
+ {
+ "type": "string",
+ "const": "density"
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "point": {
"anyOf": [
{
"type": "boolean"
},
{
- "$ref": "#/definitions/TrendlineConfig"
+ "type": "string",
+ "const": "transparent"
+ },
+ {
+ "type": "string",
+ "const": "endpoints"
+ },
+ {
+ "type": "string",
+ "const": "last"
+ },
+ {
+ "type": "string",
+ "const": "first"
}
],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
+ "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
},
- "align": {
+ "interpolate": {
"type": "string",
"enum": [
- "left",
- "center",
- "right"
+ "linear",
+ "monotone",
+ "step",
+ "step-before",
+ "step-after",
+ "basis",
+ "cardinal",
+ "natural"
],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
+ "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
},
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
+ "bandwidth": {
+ "type": "number",
+ "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
},
- "fontSize": {
+ "cumulative": {
+ "type": "boolean",
+ "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
+ },
+ "steps": {
"type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
+ "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
},
- "fillPattern": {
+ "type": {
"type": "string",
- "enum": [
- "auto",
- "none"
- ],
- "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
+ "const": "density"
},
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
+ "size": {
+ "type": "number",
+ "description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
},
- "colorByDirection": {
+ "filled": {
"type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
+ "description": "Whether the mark is filled (vs stroked only)."
},
- "units": {
+ "opacity": {
"type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
+ "description": "Default opacity (0-1)."
},
- "columns": {
+ "fillOpacity": {
"type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
+ "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "other": {
+ "fill": {
"anyOf": [
{
- "type": "number"
+ "type": "string"
},
{
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
+ "$ref": "#/definitions/GradientDef"
}
],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
+ "description": "Default fill color or gradient."
},
- "weekStart": {
+ "stroke": {
"type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
+ "description": "Default stroke color."
},
- "cellRadius": {
+ "strokeWidth": {
"type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
+ "description": "Default stroke width."
},
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
+ "tooltip": {
+ "type": [
+ "boolean",
+ "null"
],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
+ "description": "Tooltip behavior. null disables tooltips."
},
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
+ "clip": {
+ "type": "boolean",
+ "description": "Clip marks to the chart area."
+ },
+ "fillPattern": {
+ "type": "string",
+ "enum": [
+ "auto",
+ "none"
],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
+ "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
}
},
"required": [
@@ -6239,30 +2092,6 @@
],
"description": "Chart specification: the primary input for standard chart types.\n\nUses the Vega-Lite `mark` property instead of `type` to specify the visualization mark. The mark can be a string shorthand or an object with additional properties (interpolation, point markers, etc.).\n\nThis is a discriminated union — the `mark` value determines which encoding channels are required. TypeScript enforces required channels at compile time, matching the runtime rules in `MARK_ENCODING_RULES`."
},
- "MarkType": {
- "type": "string",
- "enum": [
- "bar",
- "line",
- "area",
- "point",
- "circle",
- "arc",
- "text",
- "rule",
- "tick",
- "rect",
- "lollipop",
- "beeswarm",
- "range",
- "waffle",
- "calendar",
- "parliament",
- "histogram",
- "density"
- ],
- "description": "Supported mark types, following Vega-Lite conventions.\n\nMapping from previous OpenChart chart types:\n- 'bar' covers both horizontal bars (old 'bar') and vertical columns (old 'column')\n- 'arc' covers both pie (old 'pie') and donut (old 'donut')\n- 'point' replaces old 'scatter'\n- 'circle' replaces old 'dot'\n- 'line' and 'area' unchanged\n\nNew mark types not in previous OpenChart:\n- 'text': data-positioned text labels\n- 'rule': reference lines as data marks\n- 'tick': strip/rug plot marks\n- 'rect': heatmaps and 2D binned plots\n- 'range': dumbbell / arrow / range-bar plots spanning two values per category\n- 'waffle': unit grids (\"x of 100\") for part-to-whole counts\n- 'calendar': GitHub-style calendar heatmap (weeks x weekdays, daily dates)\n- 'parliament': hemicycle seat-dot chart for election/legislature results\n- 'histogram': binned bars over a continuous field; sugar for a bar with `x.bin` + `y: { aggregate: 'count' }`\n- 'density': kernel density estimate drawn as a filled curve; sugar for an area over a density transform"
- },
"GradientDef": {
"anyOf": [
{
@@ -6381,29 +2210,6 @@
"additionalProperties": false,
"description": "Radial gradient definition. Coordinates are in [0,1] normalized space relative to the mark's bounding box."
},
- "TrendlineConfig": {
- "type": "object",
- "properties": {
- "layer": {
- "type": "string",
- "enum": [
- "above",
- "below"
- ],
- "description": "Stacking order relative to the point marks. `'below'` (default) draws the line behind the dots; `'above'` draws it on top so it stays visible over a dense cloud."
- },
- "stroke": {
- "type": "string",
- "description": "Stroke color. Defaults to the theme's high-contrast text color so the line reads on both light and dark grounds. Set an explicit CSS color to override."
- },
- "strokeWidth": {
- "type": "number",
- "description": "Stroke width in pixels. Defaults to 1.5. Over a dense cloud with `layer: 'above'`, a heavier line (e.g. 2.5) stays legible where the thin default disappears into the dots."
- }
- },
- "additionalProperties": false,
- "description": "Configuration for the scatter regression trend line. Passed as the object form of `MarkDef.trendline`."
- },
"ArcEncoding": {
"type": "object",
"properties": {
@@ -6518,7 +2324,7 @@
"color"
],
"additionalProperties": false,
- "description": "Encoding for arc marks (pie/donut charts).\n- `theta`: required (quantitative — the canonical slice value channel)\n- `color`: required (nominal/ordinal — the category)\n- `y`: deprecated alias for `theta`, accepted for backward compatibility"
+ "description": "Encoding for arc marks (pie/donut charts).\n- `theta`: required (quantitative — the canonical slice value channel)\n- `color`: required (nominal/ordinal — the category)"
},
"EncodingChannel": {
"type": "object",
@@ -6771,6 +2577,19 @@
"type": "number",
"description": "Axis position offset in pixels."
},
+ "titleAngle": {
+ "type": "number",
+ "description": "Angle of the title in degrees. A y-axis title at angle 0 and anchor end becomes a header."
+ },
+ "titleAnchor": {
+ "type": "string",
+ "enum": [
+ "start",
+ "middle",
+ "end"
+ ],
+ "description": "Position along the axis for the title; omission keeps the centered title."
+ },
"titlePadding": {
"type": "number",
"description": "Padding between axis title and axis."
@@ -8068,10 +3887,6 @@
"type": "string",
"description": "Stable identifier for selection and edit callbacks. When provided, edit events include this ID for reliable element matching."
},
- "label": {
- "type": "string",
- "description": "Human-readable label for the annotation."
- },
"fill": {
"type": "string",
"description": "Fill color for the annotation element."
@@ -8120,18 +3935,18 @@
},
"subtitle": {
"type": "string",
- "description": "Optional muted second-tone text rendered below the primary `text`. Used for supporting context (e.g. methodology, source). Newlines in `text` still produce multi-line primary; subtitle is a separate block.\n\nSupports the same inline `**bold**` spans as `text`. When a subtitle is present and no `fontWeight` is set, the primary `text` resolves to weight 700 — the lede + context stack."
+ "description": "Optional muted second-tone text rendered below the primary `text`. Used for supporting context (e.g. methodology, source). Newlines in `text` still produce multi-line primary; subtitle is a separate block.\n\nSupports the same inline `**bold**` spans as `text`. The primary `text` keeps its own weight (default 400); use `fontWeight` or `**bold**` spans for a bold lede."
},
"subtitleFontSize": {
"type": "number",
- "description": "Subtitle size in pixels. Default: round(fontSize * 0.85)."
+ "description": "Subtitle size in pixels. Default: round(fontSize * 12 / 13)."
},
"maxWidth": {
"type": [
"number",
"null"
],
- "description": "Optional display wrap width in pixels. Omitted or null keeps unbounded lines."
+ "description": "Display wrap width in pixels. Omitted uses min(240, plot width minus twice the padding); null disables wrapping."
},
"dot": {
"anyOf": [
@@ -8172,7 +3987,7 @@
"$ref": "#/definitions/ConnectorConfig"
}
],
- "description": "Connector from label to anchor point.\n- `true` (default): straight line, no arrow\n- `'straight'`: straight line, no arrow\n- `'curve'`: curved arrow with arrowhead\n- `'drop-line'`: vertical line through the data point's x\n- `{ type, arrow? }`: object form for explicit arrow control\n- `false`: no connector\n\nThe connector leaves the text block on the side facing the data point, with a small standoff gap. A leader shorter than 8px is dropped as noise (the marker alone reads better), as is a connector whose target sits inside the text block. Widen `offset` if you want the line back."
+ "description": "Connector from label to anchor point.\n- `true` (default): straight line, no arrow\n- `'straight'`: straight line, no arrow\n- `'curve'`: curved leader; set arrow: true for an arrowhead\n- `'drop-line'`: vertical line through the data point's x\n- `{ type, arrow? }`: object form for explicit arrow control\n- `false`: no connector\n\nThe connector leaves the text block on the side facing the data point, with a small standoff gap. A leader shorter than 8px is dropped as noise (the marker alone reads better), as is a connector whose target sits inside the text block. Widen `offset` if you want the line back."
},
"connectorOffset": {
"type": "object",
@@ -8224,7 +4039,7 @@
},
"fill": {
"type": "string",
- "description": "Fill color. Defaults to theme background for an \"open ring\" look."
+ "description": "Fill color. Defaults to `theme.colors.annotationMarkerFill`, which resolves to the theme's opaque surface. A default dot on an endpoint marker takes the marker's fill and stroke."
},
"stroke": {
"type": "string",
@@ -8271,7 +4086,7 @@
},
"arrow": {
"type": "boolean",
- "description": "Whether to draw an arrowhead at the data-point end. Default: true for curve, false for straight. Drop-line never takes a head."
+ "description": "Whether to draw an arrowhead at the data-point end. Default: false for both curve and straight. Drop-line never takes a head."
}
},
"required": [
@@ -8287,10 +4102,6 @@
"type": "string",
"description": "Stable identifier for selection and edit callbacks. When provided, edit events include this ID for reliable element matching."
},
- "label": {
- "type": "string",
- "description": "Human-readable label for the annotation."
- },
"fill": {
"type": "string",
"description": "Fill color for the annotation element."
@@ -8315,6 +4126,10 @@
"type": "string",
"const": "range"
},
+ "label": {
+ "type": "string",
+ "description": "Text drawn with the range band."
+ },
"x1": {
"type": [
"string",
@@ -8357,7 +4172,7 @@
},
"fontSize": {
"type": "number",
- "description": "Font size override for the range label (px). Default: 11."
+ "description": "Font size override for the range label (px). Default: theme small (12px)."
},
"fontWeight": {
"type": "number",
@@ -8377,10 +4192,6 @@
"type": "string",
"description": "Stable identifier for selection and edit callbacks. When provided, edit events include this ID for reliable element matching."
},
- "label": {
- "type": "string",
- "description": "Human-readable label for the annotation."
- },
"fill": {
"type": "string",
"description": "Fill color for the annotation element."
@@ -8404,7 +4215,11 @@
"type": {
"type": "string",
"const": "refline",
- "description": "Discriminant. `'refline'` is canonical and the only value in the type. `'rule'` (it collides with the `rule` mark type) is still accepted at runtime for backward compatibility -- the spec-sugar pass rewrites it to `'refline'` with a deprecation warning before validation -- but it is not part of this type. Use `'refline'`."
+ "description": "Reference-line annotation discriminant. Older rule aliases require migrateSpecV8."
+ },
+ "label": {
+ "type": "string",
+ "description": "Text drawn with the reference line."
},
"x": {
"type": [
@@ -8452,7 +4267,7 @@
"end",
"outside"
],
- "description": "Opt-in screen-space label placement. Outside reserves a right gutter or top lane; omission preserves legacy labelAnchor behavior."
+ "description": "Screen-space label placement. Outside reserves a right gutter or top lane; omission uses auto unless labelAnchor is authored."
},
"labelAnchor": {
"$ref": "#/definitions/AnnotationAnchor",
@@ -8460,7 +4275,7 @@
},
"fontSize": {
"type": "number",
- "description": "Label font size in pixels. Default: 11."
+ "description": "Label font size in pixels. Default: theme small (12px)."
},
"fontWeight": {
"type": "number",
@@ -8487,6 +4302,16 @@
"LabelConfig": {
"type": "object",
"properties": {
+ "placement": {
+ "type": "string",
+ "enum": [
+ "adaptive",
+ "inside",
+ "outside",
+ "column"
+ ],
+ "description": "Placement for bar/column values. Column is only supported for simple horizontal bars."
+ },
"density": {
"$ref": "#/definitions/LabelDensity",
"description": "How many labels to show. Defaults to 'auto'."
@@ -8507,14 +4332,6 @@
"type": "string",
"description": "Fixed CSS color for all labels. Overrides the default fill-derived color."
},
- "offsets": {
- "type": "object",
- "additionalProperties": {
- "$ref": "#/definitions/AnnotationOffset"
- },
- "description": "Per-series pixel offsets for fine-tuning label positions, keyed by series name.",
- "deprecated": "Per-series pixel offsets are a workaround for collision bugs\nnow fixed by the layout engine. Will be removed in a future version."
- },
"fontSize": {
"type": "number",
"description": "Font size in pixels for bar/column value labels."
@@ -8531,11 +4348,27 @@
"endpoints",
"none"
],
- "description": "Label density mode controlling how many data labels are shown.\n- 'all': show every label, skip collision detection\n- 'auto': show labels with collision detection (default)\n- 'endpoints': show only first and last per series (useful for line charts)\n- 'none': hide all labels (rely on tooltips and legend)"
+ "description": "Label density mode controlling how many data labels are shown.\n- 'all': show every label, skip collision detection\n- 'auto': show labels with collision detection (default)\n- 'endpoints': show only first and last per series\n\nLine and area marks draw no value labels; identify their series with `endpointLabels` or the legend.\n- 'none': hide all labels (rely on tooltips and legend)"
},
"EndpointLabelsConfig": {
"type": "object",
"properties": {
+ "anchor": {
+ "type": "string",
+ "enum": [
+ "boundary",
+ "band"
+ ],
+ "description": "Stacked-area anchor: cumulative boundary or the middle of the band. Default band."
+ },
+ "placement": {
+ "type": "string",
+ "enum": [
+ "end",
+ "interior"
+ ],
+ "description": "Series identification in the end column or fitted inside stacked bands."
+ },
"show": {
"type": "boolean",
"description": "Explicit on/off. When undefined, the chart auto-decides based on series count."
@@ -8586,7 +4419,7 @@
},
"showMarker": {
"type": "boolean",
- "description": "Render an open-circle marker on the line at the right edge. Default true."
+ "description": "Render a ring marker centered on the line's end point. Default true. Its fill defaults to the opaque canvas color so the ring covers the line end."
},
"markerStyle": {
"type": "object",
@@ -8605,7 +4438,7 @@
}
},
"additionalProperties": false,
- "description": "Override the marker style."
+ "description": "Override the marker style. `fill: 'transparent'` keeps a see-through ring."
},
"showLeader": {
"type": "boolean",
@@ -8613,7 +4446,7 @@
}
},
"additionalProperties": false,
- "description": "Configuration for the endpoint labels column rendered at the chart's right edge for multi-series line/area charts. Each entry pairs the series name with its last formatted value, optionally anchored to the line by an open-circle marker.\n\nThe column is independent of the traditional `legend` and the legacy end-of-line labels. Together with `legend.show`, the three suppression toggles follow this truth table for ≥2-series line/area charts:\n\n| `legend.show` | `endpointLabels` | Traditional legend | Endpoint column | End-of-line labels | |--|--|--|--|--| | unset | unset | hidden (auto-suppressed) | shown (default) | hidden | | true | unset | shown | shown | hidden | | unset | false | shown (auto-suppress revoked) | hidden | hidden | | false | false | hidden | hidden | shown (last-resort) | | true | false | shown | hidden | hidden | | false | true | hidden | shown | hidden | | true | true | shown | shown | hidden |\n\nSingle-series charts: column is hidden by default (nothing to identify).\n\nThe implementation is in `packages/engine/src/legend/suppression.ts` — `resolveSuppression` is the single source of truth. The table above is a user-facing mirror; if the two ever diverge, the engine wins. Tests in `legend/__tests__/suppression.test.ts` enforce every cell."
+ "description": "Configuration for the endpoint labels column rendered at the chart's right edge for multi-series line/area charts. Each entry pairs the series name with its last formatted value, optionally anchored to the line by an open-circle marker.\n\nThe column and traditional legend are independent identification surfaces. For multi-series line/area charts:\n\n| `legend.show` | `endpointLabels` | Traditional legend | Endpoint column | |--|--|--|--| | unset | unset | hidden | shown | | true | unset | shown | shown | | unset | false | shown | hidden | | false | false | hidden (accessibility warning) | hidden | | true | false | shown | hidden | | false | true | hidden | shown | | true | true | shown | shown |\n\nSingle-series charts: column is hidden by default (nothing to identify).\n\nThe implementation is in `packages/engine/src/legend/suppression.ts` — `resolveSuppression` is the single source of truth. The table above is a user-facing mirror; if the two ever diverge, the engine wins. Tests in `legend/__tests__/suppression.test.ts` enforce every cell."
},
"EndpointLabelContent": {
"type": "string",
@@ -8632,7 +4465,7 @@
"number",
"null"
],
- "description": "Maximum eligible inline callouts below 400px container width; null or omitted is unlimited. Pinned annotations are exempt."
+ "description": "Maximum eligible inline callouts below 400px container width; default 1; null is unlimited. Pinned annotations are exempt."
},
"preserveEnd": {
"type": "boolean",
@@ -8754,6 +4587,22 @@
"$ref": "#/definitions/TokenValue",
"description": "Annotation plate color. Accepts a TokenValue light/dark pair."
},
+ "gridlineZero": {
+ "$ref": "#/definitions/TokenValue",
+ "description": "Optional map missing-data and geographic border ink."
+ },
+ "mapNoDataFill": {
+ "$ref": "#/definitions/TokenValue"
+ },
+ "mapNoDataHatch": {
+ "$ref": "#/definitions/TokenValue"
+ },
+ "mapInteriorBorder": {
+ "$ref": "#/definitions/TokenValue"
+ },
+ "mapOutlineBorder": {
+ "$ref": "#/definitions/TokenValue"
+ },
"positive": {
"$ref": "#/definitions/TokenValue",
"description": "Semantic color for positive/up-trend values. Accepts a TokenValue for light/dark pairs."
@@ -8796,11 +4645,11 @@
},
"small": {
"type": "number",
- "description": "Small text (source line, footer). Default: 11."
+ "description": "Small text (source line, footer). Default: 12."
},
"axisTick": {
"type": "number",
- "description": "Axis tick labels. Default: 11."
+ "description": "Axis tick labels. Default: 12."
},
"metricLabel": {
"type": "number",
@@ -8809,6 +4658,13 @@
"metricValue": {
"type": "number",
"description": "KPI metric primary value. Default: 22. Delta/secondary derive from this."
+ },
+ "endpointLabel": {
+ "type": "number",
+ "description": "Series-name and numerical endpoint font sizes in pixels."
+ },
+ "endpointValue": {
+ "type": "number"
}
},
"additionalProperties": false,
@@ -8871,6 +4727,16 @@
"xAxisLabelPadding": {
"type": "number",
"description": "Gap in pixels between the x-axis line and tick label text. Increase when larger axisTick fonts sit too close to the axis line."
+ },
+ "endpointEntryGap": {
+ "type": "number",
+ "description": "Endpoint block separation, internal value gap, and plot-to-column gap."
+ },
+ "endpointValueGap": {
+ "type": "number"
+ },
+ "endpointColumnGap": {
+ "type": "number"
}
},
"additionalProperties": false,
@@ -8884,97 +4750,57 @@
"type": "object",
"properties": {
"eyebrow": {
- "anyOf": [
- {
- "$ref": "#/definitions/ChromeThemeOverride"
- },
- {
- "type": "string"
- }
- ],
+ "$ref": "#/definitions/ChromeThemeOverride",
"description": "Eyebrow (kicker) style override."
},
"title": {
- "anyOf": [
- {
- "$ref": "#/definitions/ChromeThemeOverride"
- },
- {
- "type": "string"
- }
- ],
+ "$ref": "#/definitions/ChromeThemeOverride",
"description": "Title style override."
},
"subtitle": {
- "anyOf": [
- {
- "$ref": "#/definitions/ChromeThemeOverride"
- },
- {
- "type": "string"
- }
- ],
+ "$ref": "#/definitions/ChromeThemeOverride",
"description": "Subtitle style override."
},
"source": {
- "anyOf": [
- {
- "$ref": "#/definitions/ChromeThemeOverride"
- },
- {
- "type": "string"
- }
- ],
+ "$ref": "#/definitions/ChromeThemeOverride",
"description": "Source/attribution style override."
},
"byline": {
- "anyOf": [
- {
- "$ref": "#/definitions/ChromeThemeOverride"
- },
- {
- "type": "string"
- }
- ],
+ "$ref": "#/definitions/ChromeThemeOverride",
"description": "Byline style override."
},
"footer": {
- "anyOf": [
- {
- "$ref": "#/definitions/ChromeThemeOverride"
- },
- {
- "type": "string"
- }
- ],
+ "$ref": "#/definitions/ChromeThemeOverride",
"description": "Footer style override."
}
},
"additionalProperties": false,
- "description": "Per-element chrome style overrides. Pass a plain string for color-only (backward-compatible), or a ChromeThemeOverride object for full typography control (color, fontSize, fontWeight, lineHeight)."
+ "description": "Per-element chrome typography control (color, fontSize, fontWeight, lineHeight)."
},
"rule": {
- "type": "object",
- "properties": {
- "color": {
- "$ref": "#/definitions/TokenValue",
- "description": "Rule color. Accepts a TokenValue for light/dark pairs."
- },
- "width": {
- "type": "number",
- "description": "Rule length in pixels."
+ "anyOf": [
+ {
+ "type": "object",
+ "properties": {
+ "color": {
+ "$ref": "#/definitions/TokenValue",
+ "description": "Rule color. Accepts a TokenValue for light/dark pairs."
+ },
+ "width": {
+ "type": "number",
+ "description": "Rule length in pixels."
+ },
+ "thickness": {
+ "type": "number",
+ "description": "Rule height in pixels."
+ }
+ },
+ "additionalProperties": false
},
- "thickness": {
- "type": "number",
- "description": "Rule height in pixels."
+ {
+ "type": "null"
}
- },
- "required": [
- "color",
- "width",
- "thickness"
],
- "additionalProperties": false,
"description": "Editorial rule above the chrome block: a short colored bar (the Economist/FT masthead device). Omit for no rule."
},
"seriesStrategy": {
@@ -9030,7 +4856,7 @@
}
},
"additionalProperties": false,
- "description": "Per-element chrome theme override. Pass a plain string (color only, backward-compatible) or an object with full typography control."
+ "description": "Per-element chrome theme typography controls."
},
"SeriesStrategy": {
"type": "string",
@@ -9040,15 +4866,6 @@
],
"description": "Series color assignment strategy.\n\n- `'palette'` — always assign from the full categorical palette (current default, zero visual drift).\n- `'accent-neutral'` — assign by series count: a single series gets the accent (first categorical color) only; 2-4 series get accent for the first and neutral grays for the rest; 5+ fall back to the full palette."
},
- "DarkMode": {
- "type": "string",
- "enum": [
- "auto",
- "force",
- "off"
- ],
- "description": "Dark mode behavior.\n\n- `\"auto\"` - Checks the `prefers-color-scheme` media query to detect the user's system-level preference. This does NOT detect class-based dark mode toggles (e.g. `document.documentElement.classList.toggle('dark')`). If your app uses class-based dark mode, use VizThemeProvider with `\"force\"` or `\"off\"` instead of `\"auto\"` and toggle based on your app's state.\n- `\"force\"` - Always render in dark mode regardless of system preference.\n- `\"off\"` - Always render in light mode (default).\n\nAll components (Chart, Sankey, Graph) inherit darkMode from VizThemeProvider when no explicit prop is passed."
- },
"SeriesStyle": {
"type": "object",
"properties": {
@@ -9088,6 +4905,17 @@
"$ref": "#/definitions/LabelSpec",
"description": "Override label configuration at this breakpoint."
},
+ "endpointLabels": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "$ref": "#/definitions/EndpointLabelsConfig"
+ }
+ ],
+ "description": "Override endpoint labels at this breakpoint, including migrated v8 line labels."
+ },
"legend": {
"$ref": "#/definitions/LegendConfig",
"description": "Override legend configuration at this breakpoint."
@@ -9137,8 +4965,7 @@
"type": "string",
"enum": [
"fluid",
- "subtle",
- "legacy"
+ "subtle"
],
"description": "Opt-in motion language."
},
@@ -9523,7 +5350,7 @@
},
"theta": {
"$ref": "#/definitions/EncodingChannel%3CDataRow%3E",
- "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). `y` is still accepted on these marks as a deprecated alias for backward compatibility (a compile warning is emitted and `y` is populated from `theta` internally). Not used by any other mark type."
+ "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). The compiler populates internal `y` from `theta`; authored `y` aliases are rejected. Not used by any other mark type."
},
"facet": {
"$ref": "#/definitions/FacetChannel%3CDataRow%3E",
@@ -9642,7 +5469,7 @@
},
"theta": {
"$ref": "#/definitions/EncodingChannel%3CDataRow%3E",
- "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). `y` is still accepted on these marks as a deprecated alias for backward compatibility (a compile warning is emitted and `y` is populated from `theta` internally). Not used by any other mark type."
+ "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). The compiler populates internal `y` from `theta`; authored `y` aliases are rejected. Not used by any other mark type."
},
"facet": {
"$ref": "#/definitions/FacetChannel%3CDataRow%3E",
@@ -9761,7 +5588,7 @@
},
"theta": {
"$ref": "#/definitions/EncodingChannel%3CDataRow%3E",
- "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). `y` is still accepted on these marks as a deprecated alias for backward compatibility (a compile warning is emitted and `y` is populated from `theta` internally). Not used by any other mark type."
+ "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). The compiler populates internal `y` from `theta`; authored `y` aliases are rejected. Not used by any other mark type."
},
"facet": {
"$ref": "#/definitions/FacetChannel%3CDataRow%3E",
@@ -9783,6 +5610,29 @@
"additionalProperties": false,
"description": "Encoding for area marks.\n- `x`: required (temporal or ordinal)\n- `y`: required (quantitative)"
},
+ "TrendlineConfig": {
+ "type": "object",
+ "properties": {
+ "layer": {
+ "type": "string",
+ "enum": [
+ "above",
+ "below"
+ ],
+ "description": "Stacking order relative to the point marks. `'below'` (default) draws the line behind the dots; `'above'` draws it on top so it stays visible over a dense cloud."
+ },
+ "stroke": {
+ "type": "string",
+ "description": "Stroke color. Defaults to the theme's high-contrast text color so the line reads on both light and dark grounds. Set an explicit CSS color to override."
+ },
+ "strokeWidth": {
+ "type": "number",
+ "description": "Stroke width in pixels. Defaults to 1.5. Over a dense cloud with `layer: 'above'`, a heavier line (e.g. 2.5) stays legible where the thin default disappears into the dots."
+ }
+ },
+ "additionalProperties": false,
+ "description": "Configuration for the scatter regression trend line. Passed as the object form of `MarkDef.trendline`."
+ },
"PointEncoding": {
"type": "object",
"properties": {
@@ -9880,7 +5730,7 @@
},
"theta": {
"$ref": "#/definitions/EncodingChannel%3CDataRow%3E",
- "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). `y` is still accepted on these marks as a deprecated alias for backward compatibility (a compile warning is emitted and `y` is populated from `theta` internally). Not used by any other mark type."
+ "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). The compiler populates internal `y` from `theta`; authored `y` aliases are rejected. Not used by any other mark type."
},
"facet": {
"$ref": "#/definitions/FacetChannel%3CDataRow%3E",
@@ -9999,7 +5849,7 @@
},
"theta": {
"$ref": "#/definitions/EncodingChannel%3CDataRow%3E",
- "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). `y` is still accepted on these marks as a deprecated alias for backward compatibility (a compile warning is emitted and `y` is populated from `theta` internally). Not used by any other mark type."
+ "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). The compiler populates internal `y` from `theta`; authored `y` aliases are rejected. Not used by any other mark type."
},
"facet": {
"$ref": "#/definitions/FacetChannel%3CDataRow%3E",
@@ -10118,7 +5968,7 @@
},
"theta": {
"$ref": "#/definitions/EncodingChannel%3CDataRow%3E",
- "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). `y` is still accepted on these marks as a deprecated alias for backward compatibility (a compile warning is emitted and `y` is populated from `theta` internally). Not used by any other mark type."
+ "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). The compiler populates internal `y` from `theta`; authored `y` aliases are rejected. Not used by any other mark type."
},
"facet": {
"$ref": "#/definitions/FacetChannel%3CDataRow%3E",
@@ -10237,7 +6087,7 @@
},
"theta": {
"$ref": "#/definitions/EncodingChannel%3CDataRow%3E",
- "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). `y` is still accepted on these marks as a deprecated alias for backward compatibility (a compile warning is emitted and `y` is populated from `theta` internally). Not used by any other mark type."
+ "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). The compiler populates internal `y` from `theta`; authored `y` aliases are rejected. Not used by any other mark type."
},
"facet": {
"$ref": "#/definitions/FacetChannel%3CDataRow%3E",
@@ -10352,7 +6202,7 @@
},
"theta": {
"$ref": "#/definitions/EncodingChannel%3CDataRow%3E",
- "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). `y` is still accepted on these marks as a deprecated alias for backward compatibility (a compile warning is emitted and `y` is populated from `theta` internally). Not used by any other mark type."
+ "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). The compiler populates internal `y` from `theta`; authored `y` aliases are rejected. Not used by any other mark type."
},
"facet": {
"$ref": "#/definitions/FacetChannel%3CDataRow%3E",
@@ -10468,7 +6318,7 @@
},
"theta": {
"$ref": "#/definitions/EncodingChannel%3CDataRow%3E",
- "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). `y` is still accepted on these marks as a deprecated alias for backward compatibility (a compile warning is emitted and `y` is populated from `theta` internally). Not used by any other mark type."
+ "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). The compiler populates internal `y` from `theta`; authored `y` aliases are rejected. Not used by any other mark type."
},
"facet": {
"$ref": "#/definitions/FacetChannel%3CDataRow%3E",
@@ -10487,7 +6337,7 @@
"color"
],
"additionalProperties": false,
- "description": "Encoding for waffle marks (unit grids for part-to-whole counts).\n- `color`: required (nominal/ordinal, the category)\n- `theta`: the canonical quantitative share, the same part-to-whole value channel arc marks use. `y` is a deprecated alias, accepted for backward compatibility."
+ "description": "Encoding for waffle marks (unit grids for part-to-whole counts).\n- `color`: required (nominal/ordinal, the category)\n- `theta`: the canonical quantitative share, the same part-to-whole value channel arc marks use."
},
"TextEncoding": {
"type": "object",
@@ -10586,7 +6436,7 @@
},
"theta": {
"$ref": "#/definitions/EncodingChannel%3CDataRow%3E",
- "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). `y` is still accepted on these marks as a deprecated alias for backward compatibility (a compile warning is emitted and `y` is populated from `theta` internally). Not used by any other mark type."
+ "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). The compiler populates internal `y` from `theta`; authored `y` aliases are rejected. Not used by any other mark type."
},
"facet": {
"$ref": "#/definitions/FacetChannel%3CDataRow%3E",
@@ -10704,7 +6554,7 @@
},
"theta": {
"$ref": "#/definitions/EncodingChannel%3CDataRow%3E",
- "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). `y` is still accepted on these marks as a deprecated alias for backward compatibility (a compile warning is emitted and `y` is populated from `theta` internally). Not used by any other mark type."
+ "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). The compiler populates internal `y` from `theta`; authored `y` aliases are rejected. Not used by any other mark type."
},
"facet": {
"$ref": "#/definitions/FacetChannel%3CDataRow%3E",
@@ -10823,7 +6673,7 @@
},
"theta": {
"$ref": "#/definitions/EncodingChannel%3CDataRow%3E",
- "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). `y` is still accepted on these marks as a deprecated alias for backward compatibility (a compile warning is emitted and `y` is populated from `theta` internally). Not used by any other mark type."
+ "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). The compiler populates internal `y` from `theta`; authored `y` aliases are rejected. Not used by any other mark type."
},
"facet": {
"$ref": "#/definitions/FacetChannel%3CDataRow%3E",
@@ -10942,7 +6792,7 @@
},
"theta": {
"$ref": "#/definitions/EncodingChannel%3CDataRow%3E",
- "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). `y` is still accepted on these marks as a deprecated alias for backward compatibility (a compile warning is emitted and `y` is populated from `theta` internally). Not used by any other mark type."
+ "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). The compiler populates internal `y` from `theta`; authored `y` aliases are rejected. Not used by any other mark type."
},
"facet": {
"$ref": "#/definitions/FacetChannel%3CDataRow%3E",
@@ -11047,7 +6897,7 @@
},
"theta": {
"$ref": "#/definitions/EncodingChannel%3CDataRow%3E",
- "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). `y` is still accepted on these marks as a deprecated alias for backward compatibility (a compile warning is emitted and `y` is populated from `theta` internally). Not used by any other mark type."
+ "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). The compiler populates internal `y` from `theta`; authored `y` aliases are rejected. Not used by any other mark type."
},
"facet": {
"$ref": "#/definitions/FacetChannel%3CDataRow%3E",
@@ -11163,7 +7013,7 @@
},
"theta": {
"$ref": "#/definitions/EncodingChannel%3CDataRow%3E",
- "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). `y` is still accepted on these marks as a deprecated alias for backward compatibility (a compile warning is emitted and `y` is populated from `theta` internally). Not used by any other mark type."
+ "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). The compiler populates internal `y` from `theta`; authored `y` aliases are rejected. Not used by any other mark type."
},
"facet": {
"$ref": "#/definitions/FacetChannel%3CDataRow%3E",
@@ -11182,7 +7032,7 @@
"color"
],
"additionalProperties": false,
- "description": "Encoding for parliament marks (hemicycle seat-dot charts).\n- `color`: required (nominal/ordinal, the party)\n- `theta`: the canonical quantitative seat count, the same part-to-whole value channel arc/waffle marks use. `y` is a deprecated alias, accepted for backward compatibility."
+ "description": "Encoding for parliament marks (hemicycle seat-dot charts).\n- `color`: required (nominal/ordinal, the party)\n- `theta`: the canonical quantitative seat count, the same part-to-whole value channel arc/waffle marks use."
},
"HistogramEncoding": {
"type": "object",
@@ -11281,7 +7131,7 @@
},
"theta": {
"$ref": "#/definitions/EncodingChannel%3CDataRow%3E",
- "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). `y` is still accepted on these marks as a deprecated alias for backward compatibility (a compile warning is emitted and `y` is populated from `theta` internally). Not used by any other mark type."
+ "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). The compiler populates internal `y` from `theta`; authored `y` aliases are rejected. Not used by any other mark type."
},
"facet": {
"$ref": "#/definitions/FacetChannel%3CDataRow%3E",
@@ -11399,7 +7249,7 @@
},
"theta": {
"$ref": "#/definitions/EncodingChannel%3CDataRow%3E",
- "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). `y` is still accepted on these marks as a deprecated alias for backward compatibility (a compile warning is emitted and `y` is populated from `theta` internally). Not used by any other mark type."
+ "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). The compiler populates internal `y` from `theta`; authored `y` aliases are rejected. Not used by any other mark type."
},
"facet": {
"$ref": "#/definitions/FacetChannel%3CDataRow%3E",
@@ -11461,7 +7311,7 @@
"subtract",
"grow"
],
- "description": "How chrome height interacts with the container height budget.\n\n- `'subtract'` (default): the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\n`'grow'` is a no-op for faceted specs in this version."
+ "description": "How chrome height interacts with the container height budget.\n\n- omitted: only notes and the series summary grow the figure; title, subtitle, and source subtract from the height budget.\n- `'subtract'`: the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\nFacets and layers report engine-owned growth once."
},
"title": {
"anyOf": [
@@ -11541,7 +7391,7 @@
"$ref": "#/definitions/EndpointLabelsConfig"
}
],
- "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show` and the legacy end-of-line labels."
+ "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show`."
},
"responsive": {
"anyOf": [
@@ -11558,10 +7408,6 @@
"$ref": "#/definitions/ThemeConfig",
"description": "Theme configuration overrides."
},
- "darkMode": {
- "$ref": "#/definitions/DarkMode",
- "deprecated": "Use the mount darkMode option; the spec field has no effect."
- },
"watermark": {
"type": "boolean",
"description": "Whether to show the OpenData watermark. Defaults to true; auto-hides when the container is under 200px tall (the cramped height class) unless explicitly set."
diff --git a/packages/core/schema/table.schema.json b/packages/core/schema/table.schema.json
index 5ac3d22e..b992ecaa 100644
--- a/packages/core/schema/table.schema.json
+++ b/packages/core/schema/table.schema.json
@@ -56,10 +56,6 @@
"$ref": "#/definitions/ThemeConfig",
"description": "Theme configuration overrides."
},
- "darkMode": {
- "$ref": "#/definitions/DarkMode",
- "deprecated": "Use the mount darkMode option; the spec field has no effect."
- },
"watermark": {
"type": "boolean",
"description": "Whether to show the OpenData watermark. Defaults to true."
@@ -121,10 +117,6 @@
],
"description": "Sticky footer summing every quantitative column over the filtered rows (the whole result set, not just the current page). `true` uses the label \"Total\"."
},
- "compact": {
- "type": "boolean",
- "deprecated": "Use `density: 'condensed'`. Still honored, with a warning."
- },
"responsive": {
"type": "boolean",
"description": "Whether the table adapts to container width. Defaults to true."
@@ -692,6 +684,22 @@
"$ref": "#/definitions/TokenValue",
"description": "Annotation plate color. Accepts a TokenValue light/dark pair."
},
+ "gridlineZero": {
+ "$ref": "#/definitions/TokenValue",
+ "description": "Optional map missing-data and geographic border ink."
+ },
+ "mapNoDataFill": {
+ "$ref": "#/definitions/TokenValue"
+ },
+ "mapNoDataHatch": {
+ "$ref": "#/definitions/TokenValue"
+ },
+ "mapInteriorBorder": {
+ "$ref": "#/definitions/TokenValue"
+ },
+ "mapOutlineBorder": {
+ "$ref": "#/definitions/TokenValue"
+ },
"positive": {
"$ref": "#/definitions/TokenValue",
"description": "Semantic color for positive/up-trend values. Accepts a TokenValue for light/dark pairs."
@@ -734,11 +742,11 @@
},
"small": {
"type": "number",
- "description": "Small text (source line, footer). Default: 11."
+ "description": "Small text (source line, footer). Default: 12."
},
"axisTick": {
"type": "number",
- "description": "Axis tick labels. Default: 11."
+ "description": "Axis tick labels. Default: 12."
},
"metricLabel": {
"type": "number",
@@ -747,6 +755,13 @@
"metricValue": {
"type": "number",
"description": "KPI metric primary value. Default: 22. Delta/secondary derive from this."
+ },
+ "endpointLabel": {
+ "type": "number",
+ "description": "Series-name and numerical endpoint font sizes in pixels."
+ },
+ "endpointValue": {
+ "type": "number"
}
},
"additionalProperties": false,
@@ -809,6 +824,16 @@
"xAxisLabelPadding": {
"type": "number",
"description": "Gap in pixels between the x-axis line and tick label text. Increase when larger axisTick fonts sit too close to the axis line."
+ },
+ "endpointEntryGap": {
+ "type": "number",
+ "description": "Endpoint block separation, internal value gap, and plot-to-column gap."
+ },
+ "endpointValueGap": {
+ "type": "number"
+ },
+ "endpointColumnGap": {
+ "type": "number"
}
},
"additionalProperties": false,
@@ -822,97 +847,57 @@
"type": "object",
"properties": {
"eyebrow": {
- "anyOf": [
- {
- "$ref": "#/definitions/ChromeThemeOverride"
- },
- {
- "type": "string"
- }
- ],
+ "$ref": "#/definitions/ChromeThemeOverride",
"description": "Eyebrow (kicker) style override."
},
"title": {
- "anyOf": [
- {
- "$ref": "#/definitions/ChromeThemeOverride"
- },
- {
- "type": "string"
- }
- ],
+ "$ref": "#/definitions/ChromeThemeOverride",
"description": "Title style override."
},
"subtitle": {
- "anyOf": [
- {
- "$ref": "#/definitions/ChromeThemeOverride"
- },
- {
- "type": "string"
- }
- ],
+ "$ref": "#/definitions/ChromeThemeOverride",
"description": "Subtitle style override."
},
"source": {
- "anyOf": [
- {
- "$ref": "#/definitions/ChromeThemeOverride"
- },
- {
- "type": "string"
- }
- ],
+ "$ref": "#/definitions/ChromeThemeOverride",
"description": "Source/attribution style override."
},
"byline": {
- "anyOf": [
- {
- "$ref": "#/definitions/ChromeThemeOverride"
- },
- {
- "type": "string"
- }
- ],
+ "$ref": "#/definitions/ChromeThemeOverride",
"description": "Byline style override."
},
"footer": {
- "anyOf": [
- {
- "$ref": "#/definitions/ChromeThemeOverride"
- },
- {
- "type": "string"
- }
- ],
+ "$ref": "#/definitions/ChromeThemeOverride",
"description": "Footer style override."
}
},
"additionalProperties": false,
- "description": "Per-element chrome style overrides. Pass a plain string for color-only (backward-compatible), or a ChromeThemeOverride object for full typography control (color, fontSize, fontWeight, lineHeight)."
+ "description": "Per-element chrome typography control (color, fontSize, fontWeight, lineHeight)."
},
"rule": {
- "type": "object",
- "properties": {
- "color": {
- "$ref": "#/definitions/TokenValue",
- "description": "Rule color. Accepts a TokenValue for light/dark pairs."
- },
- "width": {
- "type": "number",
- "description": "Rule length in pixels."
+ "anyOf": [
+ {
+ "type": "object",
+ "properties": {
+ "color": {
+ "$ref": "#/definitions/TokenValue",
+ "description": "Rule color. Accepts a TokenValue for light/dark pairs."
+ },
+ "width": {
+ "type": "number",
+ "description": "Rule length in pixels."
+ },
+ "thickness": {
+ "type": "number",
+ "description": "Rule height in pixels."
+ }
+ },
+ "additionalProperties": false
},
- "thickness": {
- "type": "number",
- "description": "Rule height in pixels."
+ {
+ "type": "null"
}
- },
- "required": [
- "color",
- "width",
- "thickness"
],
- "additionalProperties": false,
"description": "Editorial rule above the chrome block: a short colored bar (the Economist/FT masthead device). Omit for no rule."
},
"seriesStrategy": {
@@ -968,7 +953,7 @@
}
},
"additionalProperties": false,
- "description": "Per-element chrome theme override. Pass a plain string (color only, backward-compatible) or an object with full typography control."
+ "description": "Per-element chrome theme typography controls."
},
"SeriesStrategy": {
"type": "string",
@@ -978,15 +963,6 @@
],
"description": "Series color assignment strategy.\n\n- `'palette'` — always assign from the full categorical palette (current default, zero visual drift).\n- `'accent-neutral'` — assign by series count: a single series gets the accent (first categorical color) only; 2-4 series get accent for the first and neutral grays for the rest; 5+ fall back to the full palette."
},
- "DarkMode": {
- "type": "string",
- "enum": [
- "auto",
- "force",
- "off"
- ],
- "description": "Dark mode behavior.\n\n- `\"auto\"` - Checks the `prefers-color-scheme` media query to detect the user's system-level preference. This does NOT detect class-based dark mode toggles (e.g. `document.documentElement.classList.toggle('dark')`). If your app uses class-based dark mode, use VizThemeProvider with `\"force\"` or `\"off\"` instead of `\"auto\"` and toggle based on your app's state.\n- `\"force\"` - Always render in dark mode regardless of system preference.\n- `\"off\"` - Always render in light mode (default).\n\nAll components (Chart, Sankey, Graph) inherit darkMode from VizThemeProvider when no explicit prop is passed."
- },
"TableDensity": {
"type": "string",
"enum": [
@@ -1037,8 +1013,7 @@
"type": "string",
"enum": [
"fluid",
- "subtle",
- "legacy"
+ "subtle"
],
"description": "Opt-in motion language."
},
diff --git a/packages/core/schema/vizspec.schema.json b/packages/core/schema/vizspec.schema.json
index 4cdabd4b..886168ba 100644
--- a/packages/core/schema/vizspec.schema.json
+++ b/packages/core/schema/vizspec.schema.json
@@ -60,56 +60,6 @@
"type": "object",
"additionalProperties": false,
"properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
- "type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
"innerRadius": {
"type": "number",
"description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
@@ -149,17 +99,9 @@
],
"description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
},
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
+ "type": {
+ "type": "string",
+ "const": "arc"
},
"size": {
"type": "number",
@@ -177,26 +119,6 @@
"type": "number",
"description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
"fill": {
"anyOf": [
{
@@ -227,47 +149,6 @@
"type": "boolean",
"description": "Clip marks to the chart area."
},
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
"fillPattern": {
"type": "string",
"enum": [
@@ -275,99 +156,6 @@
"none"
],
"description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
}
},
"required": [
@@ -406,10 +194,6 @@
"type": "object",
"additionalProperties": false,
"properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
"point": {
"anyOf": [
{
@@ -448,52 +232,108 @@
],
"description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
},
- "orient": {
+ "type": {
"type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
+ "const": "line"
},
- "innerRadius": {
+ "size": {
"type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
+ "description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
},
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
+ "filled": {
+ "type": "boolean",
+ "description": "Whether the mark is filled (vs stroked only)."
},
- "startAngle": {
+ "opacity": {
"type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
+ "description": "Default opacity (0-1)."
},
- "endAngle": {
+ "fillOpacity": {
"type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
+ "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "centerLabel": {
+ "fill": {
"anyOf": [
{
"type": "string"
},
{
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
+ "$ref": "#/definitions/GradientDef"
}
],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
+ "description": "Default fill color or gradient."
+ },
+ "stroke": {
+ "type": "string",
+ "description": "Default stroke color."
+ },
+ "strokeWidth": {
+ "type": "number",
+ "description": "Default stroke width."
+ },
+ "tooltip": {
+ "type": [
+ "boolean",
+ "null"
+ ],
+ "description": "Tooltip behavior. null disables tooltips."
+ },
+ "clip": {
+ "type": "boolean",
+ "description": "Clip marks to the chart area."
+ },
+ "fillPattern": {
+ "type": "string",
+ "enum": [
+ "auto",
+ "none"
+ ],
+ "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
+ }
+ },
+ "required": [
+ "type"
+ ]
+ }
+ ]
+ },
+ "encoding": {
+ "$ref": "#/definitions/LineEncoding%3CDataRow%3E"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/definitions/ChartSpecBase"
+ },
+ {
+ "type": "object",
+ "required": [
+ "data",
+ "encoding",
+ "mark"
+ ],
+ "properties": {
+ "mark": {
+ "anyOf": [
+ {
+ "type": "string",
+ "const": "bar"
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "orient": {
+ "type": "string",
+ "enum": [
+ "horizontal",
+ "vertical"
+ ],
+ "description": "Explicit orientation override for bar marks."
},
"cornerRadius": {
"anyOf": [
@@ -507,6 +347,10 @@
],
"description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
},
+ "type": {
+ "type": "string",
+ "const": "bar"
+ },
"size": {
"type": "number",
"description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
@@ -523,26 +367,6 @@
"type": "number",
"description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
"fill": {
"anyOf": [
{
@@ -573,47 +397,6 @@
"type": "boolean",
"description": "Clip marks to the chart area."
},
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
"fillPattern": {
"type": "string",
"enum": [
@@ -621,99 +404,6 @@
"none"
],
"description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
}
},
"required": [
@@ -723,7 +413,7 @@
]
},
"encoding": {
- "$ref": "#/definitions/LineEncoding%3CDataRow%3E"
+ "$ref": "#/definitions/BarEncoding%3CDataRow%3E"
}
}
}
@@ -746,16 +436,12 @@
"anyOf": [
{
"type": "string",
- "const": "bar"
+ "const": "area"
},
{
"type": "object",
"additionalProperties": false,
"properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
"point": {
"anyOf": [
{
@@ -794,72 +480,17 @@
],
"description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
},
- "orient": {
+ "type": {
"type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
+ "const": "area"
},
- "innerRadius": {
+ "size": {
"type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
+ "description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
},
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
- },
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
- },
- "size": {
- "type": "number",
- "description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
- },
- "filled": {
- "type": "boolean",
- "description": "Whether the mark is filled (vs stroked only)."
+ "filled": {
+ "type": "boolean",
+ "description": "Whether the mark is filled (vs stroked only)."
},
"opacity": {
"type": "number",
@@ -869,26 +500,6 @@
"type": "number",
"description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
"fill": {
"anyOf": [
{
@@ -919,6 +530,51 @@
"type": "boolean",
"description": "Clip marks to the chart area."
},
+ "fillPattern": {
+ "type": "string",
+ "enum": [
+ "auto",
+ "none"
+ ],
+ "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
+ }
+ },
+ "required": [
+ "type"
+ ]
+ }
+ ]
+ },
+ "encoding": {
+ "$ref": "#/definitions/AreaEncoding%3CDataRow%3E"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/definitions/ChartSpecBase"
+ },
+ {
+ "type": "object",
+ "required": [
+ "data",
+ "encoding",
+ "mark"
+ ],
+ "properties": {
+ "mark": {
+ "anyOf": [
+ {
+ "type": "string",
+ "const": "point"
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
"trendline": {
"anyOf": [
{
@@ -930,136 +586,63 @@
],
"description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
},
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
+ "type": {
"type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
+ "const": "point"
},
- "fontSize": {
+ "size": {
"type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
- "fillPattern": {
- "type": "string",
- "enum": [
- "auto",
- "none"
- ],
- "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
+ "description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
},
- "colorByDirection": {
+ "filled": {
"type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
+ "description": "Whether the mark is filled (vs stroked only)."
},
- "units": {
+ "opacity": {
"type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
+ "description": "Default opacity (0-1)."
},
- "columns": {
+ "fillOpacity": {
"type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
+ "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "other": {
+ "fill": {
"anyOf": [
{
- "type": "number"
+ "type": "string"
},
{
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
+ "$ref": "#/definitions/GradientDef"
}
],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
+ "description": "Default fill color or gradient."
},
- "weekStart": {
+ "stroke": {
"type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
+ "description": "Default stroke color."
},
- "cellRadius": {
+ "strokeWidth": {
"type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
+ "description": "Default stroke width."
},
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
+ "tooltip": {
+ "type": [
+ "boolean",
+ "null"
],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
+ "description": "Tooltip behavior. null disables tooltips."
},
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
+ "clip": {
+ "type": "boolean",
+ "description": "Clip marks to the chart area."
+ },
+ "fillPattern": {
+ "type": "string",
+ "enum": [
+ "auto",
+ "none"
],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
+ "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
}
},
"required": [
@@ -1069,7 +652,7 @@
]
},
"encoding": {
- "$ref": "#/definitions/BarEncoding%3CDataRow%3E"
+ "$ref": "#/definitions/PointEncoding%3CDataRow%3E"
}
}
}
@@ -1092,112 +675,15 @@
"anyOf": [
{
"type": "string",
- "const": "area"
+ "const": "circle"
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
"type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
- },
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
+ "const": "circle"
},
"size": {
"type": "number",
@@ -1215,26 +701,6 @@
"type": "number",
"description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
"fill": {
"anyOf": [
{
@@ -1265,47 +731,6 @@
"type": "boolean",
"description": "Clip marks to the chart area."
},
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
"fillPattern": {
"type": "string",
"enum": [
@@ -1313,99 +738,6 @@
"none"
],
"description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
}
},
"required": [
@@ -1415,7 +747,7 @@
]
},
"encoding": {
- "$ref": "#/definitions/AreaEncoding%3CDataRow%3E"
+ "$ref": "#/definitions/CircleEncoding%3CDataRow%3E"
}
}
}
@@ -1438,112 +770,15 @@
"anyOf": [
{
"type": "string",
- "const": "point"
+ "const": "lollipop"
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
"type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
- },
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
+ "const": "lollipop"
},
"size": {
"type": "number",
@@ -1561,26 +796,6 @@
"type": "number",
"description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
"fill": {
"anyOf": [
{
@@ -1611,47 +826,6 @@
"type": "boolean",
"description": "Clip marks to the chart area."
},
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
"fillPattern": {
"type": "string",
"enum": [
@@ -1659,99 +833,6 @@
"none"
],
"description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
}
},
"required": [
@@ -1761,7 +842,7 @@
]
},
"encoding": {
- "$ref": "#/definitions/PointEncoding%3CDataRow%3E"
+ "$ref": "#/definitions/LollipopEncoding%3CDataRow%3E"
}
}
}
@@ -1784,112 +865,15 @@
"anyOf": [
{
"type": "string",
- "const": "circle"
+ "const": "beeswarm"
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
- "type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
- },
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
+ "type": "string",
+ "const": "beeswarm"
},
"size": {
"type": "number",
@@ -1907,26 +891,6 @@
"type": "number",
"description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
"fill": {
"anyOf": [
{
@@ -1957,47 +921,6 @@
"type": "boolean",
"description": "Clip marks to the chart area."
},
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
"fillPattern": {
"type": "string",
"enum": [
@@ -2005,99 +928,6 @@
"none"
],
"description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
}
},
"required": [
@@ -2107,7 +937,7 @@
]
},
"encoding": {
- "$ref": "#/definitions/CircleEncoding%3CDataRow%3E"
+ "$ref": "#/definitions/BeeswarmEncoding%3CDataRow%3E"
}
}
}
@@ -2130,100 +960,150 @@
"anyOf": [
{
"type": "string",
- "const": "lollipop"
+ "const": "range"
},
{
"type": "object",
"additionalProperties": false,
"properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
+ "style": {
+ "type": "string",
+ "enum": [
+ "dumbbell",
+ "arrow",
+ "bar"
+ ],
+ "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
+ },
+ "colorByDirection": {
+ "type": "boolean",
+ "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
+ },
+ "type": {
+ "type": "string",
+ "const": "range"
+ },
+ "size": {
+ "type": "number",
+ "description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
+ },
+ "filled": {
+ "type": "boolean",
+ "description": "Whether the mark is filled (vs stroked only)."
+ },
+ "opacity": {
+ "type": "number",
+ "description": "Default opacity (0-1)."
+ },
+ "fillOpacity": {
+ "type": "number",
+ "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
+ },
+ "fill": {
"anyOf": [
{
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
+ "type": "string"
},
{
- "type": "string",
- "const": "first"
+ "$ref": "#/definitions/GradientDef"
}
],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
+ "description": "Default fill color or gradient."
},
- "interpolate": {
+ "stroke": {
"type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
+ "description": "Default stroke color."
+ },
+ "strokeWidth": {
+ "type": "number",
+ "description": "Default stroke width."
+ },
+ "tooltip": {
+ "type": [
+ "boolean",
+ "null"
],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
+ "description": "Tooltip behavior. null disables tooltips."
},
- "orient": {
+ "clip": {
+ "type": "boolean",
+ "description": "Clip marks to the chart area."
+ },
+ "fillPattern": {
"type": "string",
"enum": [
- "horizontal",
- "vertical"
+ "auto",
+ "none"
],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
+ "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
+ }
+ },
+ "required": [
+ "type"
+ ]
+ }
+ ]
+ },
+ "encoding": {
+ "$ref": "#/definitions/RangeEncoding%3CDataRow%3E"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/definitions/ChartSpecBase"
+ },
+ {
+ "type": "object",
+ "required": [
+ "data",
+ "encoding",
+ "mark"
+ ],
+ "properties": {
+ "mark": {
+ "anyOf": [
+ {
+ "type": "string",
+ "const": "waffle"
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "units": {
"type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
+ "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
},
- "endAngle": {
+ "columns": {
"type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
+ "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
},
- "centerLabel": {
+ "other": {
"anyOf": [
{
- "type": "string"
+ "type": "number"
},
{
"type": "object",
"properties": {
- "text": {
- "type": "string"
+ "threshold": {
+ "type": "number"
},
- "subtitle": {
+ "label": {
"type": "string"
}
},
"required": [
- "text"
+ "threshold"
],
"additionalProperties": false
}
],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
+ "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
},
"cornerRadius": {
"anyOf": [
@@ -2237,6 +1117,10 @@
],
"description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
},
+ "type": {
+ "type": "string",
+ "const": "waffle"
+ },
"size": {
"type": "number",
"description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
@@ -2253,26 +1137,6 @@
"type": "number",
"description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
"fill": {
"anyOf": [
{
@@ -2303,17 +1167,51 @@
"type": "boolean",
"description": "Clip marks to the chart area."
},
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
+ "fillPattern": {
+ "type": "string",
+ "enum": [
+ "auto",
+ "none"
],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
+ "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
+ }
+ },
+ "required": [
+ "type"
+ ]
+ }
+ ]
+ },
+ "encoding": {
+ "$ref": "#/definitions/WaffleEncoding%3CDataRow%3E"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/definitions/ChartSpecBase"
+ },
+ {
+ "type": "object",
+ "required": [
+ "data",
+ "encoding",
+ "mark"
+ ],
+ "properties": {
+ "mark": {
+ "anyOf": [
+ {
+ "type": "string",
+ "const": "text"
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
"dx": {
"type": "number",
"description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
@@ -2344,106 +1242,63 @@
"type": "number",
"description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
},
- "fillPattern": {
+ "type": {
"type": "string",
- "enum": [
- "auto",
- "none"
- ],
- "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
+ "const": "text"
},
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
+ "size": {
+ "type": "number",
+ "description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
},
- "colorByDirection": {
+ "filled": {
"type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
+ "description": "Whether the mark is filled (vs stroked only)."
},
- "units": {
+ "opacity": {
"type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
+ "description": "Default opacity (0-1)."
},
- "columns": {
+ "fillOpacity": {
"type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
+ "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "other": {
+ "fill": {
"anyOf": [
{
- "type": "number"
+ "type": "string"
},
{
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
+ "$ref": "#/definitions/GradientDef"
}
],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
+ "description": "Default fill color or gradient."
},
- "weekStart": {
+ "stroke": {
"type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
+ "description": "Default stroke color."
},
- "cellRadius": {
+ "strokeWidth": {
"type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
+ "description": "Default stroke width."
},
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
+ "tooltip": {
+ "type": [
+ "boolean",
+ "null"
],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
+ "description": "Tooltip behavior. null disables tooltips."
},
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
+ "clip": {
+ "type": "boolean",
+ "description": "Clip marks to the chart area."
+ },
+ "fillPattern": {
+ "type": "string",
+ "enum": [
+ "auto",
+ "none"
],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
+ "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
}
},
"required": [
@@ -2453,7 +1308,7 @@
]
},
"encoding": {
- "$ref": "#/definitions/LollipopEncoding%3CDataRow%3E"
+ "$ref": "#/definitions/TextEncoding%3CDataRow%3E"
}
}
}
@@ -2476,112 +1331,15 @@
"anyOf": [
{
"type": "string",
- "const": "beeswarm"
+ "const": "tick"
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
"type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
- },
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
+ "const": "tick"
},
"size": {
"type": "number",
@@ -2599,26 +1357,6 @@
"type": "number",
"description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
"fill": {
"anyOf": [
{
@@ -2649,47 +1387,6 @@
"type": "boolean",
"description": "Clip marks to the chart area."
},
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
"fillPattern": {
"type": "string",
"enum": [
@@ -2697,99 +1394,6 @@
"none"
],
"description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
}
},
"required": [
@@ -2799,7 +1403,7 @@
]
},
"encoding": {
- "$ref": "#/definitions/BeeswarmEncoding%3CDataRow%3E"
+ "$ref": "#/definitions/TickEncoding%3CDataRow%3E"
}
}
}
@@ -2822,101 +1426,12 @@
"anyOf": [
{
"type": "string",
- "const": "range"
+ "const": "rect"
},
{
"type": "object",
"additionalProperties": false,
"properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
- "type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
- },
"cornerRadius": {
"anyOf": [
{
@@ -2929,6 +1444,10 @@
],
"description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
},
+ "type": {
+ "type": "string",
+ "const": "rect"
+ },
"size": {
"type": "number",
"description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
@@ -2945,26 +1464,6 @@
"type": "number",
"description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
"fill": {
"anyOf": [
{
@@ -2995,47 +1494,6 @@
"type": "boolean",
"description": "Clip marks to the chart area."
},
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
"fillPattern": {
"type": "string",
"enum": [
@@ -3043,99 +1501,6 @@
"none"
],
"description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
}
},
"required": [
@@ -3145,7 +1510,7 @@
]
},
"encoding": {
- "$ref": "#/definitions/RangeEncoding%3CDataRow%3E"
+ "$ref": "#/definitions/RectEncoding%3CDataRow%3E"
}
}
}
@@ -3168,112 +1533,15 @@
"anyOf": [
{
"type": "string",
- "const": "waffle"
+ "const": "rule"
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
"type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
- },
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
+ "const": "rule"
},
"size": {
"type": "number",
@@ -3291,26 +1559,6 @@
"type": "number",
"description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
"fill": {
"anyOf": [
{
@@ -3341,47 +1589,6 @@
"type": "boolean",
"description": "Clip marks to the chart area."
},
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
"fillPattern": {
"type": "string",
"enum": [
@@ -3389,51 +1596,44 @@
"none"
],
"description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
+ }
+ },
+ "required": [
+ "type"
+ ]
+ }
+ ]
+ },
+ "encoding": {
+ "$ref": "#/definitions/Encoding%3CDataRow%3E"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/definitions/ChartSpecBase"
+ },
+ {
+ "type": "object",
+ "required": [
+ "data",
+ "encoding",
+ "mark"
+ ],
+ "properties": {
+ "mark": {
+ "anyOf": [
+ {
+ "type": "string",
+ "const": "calendar"
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
"weekStart": {
"type": "string",
"enum": [
@@ -3446,42 +1646,63 @@
"type": "number",
"description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
},
- "shape": {
+ "type": {
"type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
+ "const": "calendar"
},
- "seatRadius": {
+ "size": {
+ "type": "number",
+ "description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
+ },
+ "filled": {
+ "type": "boolean",
+ "description": "Whether the mark is filled (vs stroked only)."
+ },
+ "opacity": {
+ "type": "number",
+ "description": "Default opacity (0-1)."
+ },
+ "fillOpacity": {
+ "type": "number",
+ "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
+ },
+ "fill": {
"anyOf": [
{
- "type": "number"
+ "type": "string"
},
{
- "type": "string",
- "const": "auto"
+ "$ref": "#/definitions/GradientDef"
}
],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
+ "description": "Default fill color or gradient."
},
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
+ "stroke": {
+ "type": "string",
+ "description": "Default stroke color."
+ },
+ "strokeWidth": {
+ "type": "number",
+ "description": "Default stroke width."
+ },
+ "tooltip": {
+ "type": [
+ "boolean",
+ "null"
],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
+ "description": "Tooltip behavior. null disables tooltips."
+ },
+ "clip": {
+ "type": "boolean",
+ "description": "Clip marks to the chart area."
+ },
+ "fillPattern": {
+ "type": "string",
+ "enum": [
+ "auto",
+ "none"
+ ],
+ "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
}
},
"required": [
@@ -3491,7 +1712,7 @@
]
},
"encoding": {
- "$ref": "#/definitions/WaffleEncoding%3CDataRow%3E"
+ "$ref": "#/definitions/CalendarEncoding%3CDataRow%3E"
}
}
}
@@ -3514,112 +1735,52 @@
"anyOf": [
{
"type": "string",
- "const": "text"
+ "const": "parliament"
},
{
"type": "object",
"additionalProperties": false,
"properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
+ "shape": {
+ "type": "string",
+ "const": "hemicycle",
+ "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
},
- "point": {
+ "seatRadius": {
"anyOf": [
{
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
+ "type": "number"
},
{
"type": "string",
- "const": "first"
+ "const": "auto"
}
],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
- "type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
+ "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
},
- "centerLabel": {
+ "majorityLine": {
"anyOf": [
{
- "type": "string"
+ "type": "boolean"
},
{
"type": "object",
"properties": {
- "text": {
- "type": "string"
+ "seats": {
+ "type": "number"
},
- "subtitle": {
+ "label": {
"type": "string"
}
},
- "required": [
- "text"
- ],
"additionalProperties": false
}
],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
+ "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
},
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
+ "type": {
+ "type": "string",
+ "const": "parliament"
},
"size": {
"type": "number",
@@ -3637,26 +1798,6 @@
"type": "number",
"description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
"fill": {
"anyOf": [
{
@@ -3687,47 +1828,6 @@
"type": "boolean",
"description": "Clip marks to the chart area."
},
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
"fillPattern": {
"type": "string",
"enum": [
@@ -3735,99 +1835,129 @@
"none"
],
"description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
+ }
+ },
+ "required": [
+ "type"
+ ]
+ }
+ ]
+ },
+ "encoding": {
+ "$ref": "#/definitions/ParliamentEncoding%3CDataRow%3E"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/definitions/ChartSpecBase"
+ },
+ {
+ "type": "object",
+ "required": [
+ "data",
+ "encoding",
+ "mark"
+ ],
+ "properties": {
+ "mark": {
+ "anyOf": [
+ {
+ "type": "string",
+ "const": "histogram"
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "orient": {
"type": "string",
"enum": [
- "dumbbell",
- "arrow",
- "bar"
+ "horizontal",
+ "vertical"
],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
+ "description": "Explicit orientation override for bar marks."
},
- "other": {
+ "cornerRadius": {
"anyOf": [
{
"type": "number"
},
{
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
+ "type": "string",
+ "const": "pill"
}
],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
+ "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
},
- "weekStart": {
+ "binCount": {
+ "type": "number",
+ "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
+ },
+ "normalize": {
+ "type": "boolean",
+ "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
+ },
+ "type": {
"type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
+ "const": "histogram"
},
- "cellRadius": {
+ "size": {
"type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
+ "description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
},
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
+ "filled": {
+ "type": "boolean",
+ "description": "Whether the mark is filled (vs stroked only)."
},
- "seatRadius": {
+ "opacity": {
+ "type": "number",
+ "description": "Default opacity (0-1)."
+ },
+ "fillOpacity": {
+ "type": "number",
+ "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
+ },
+ "fill": {
"anyOf": [
{
- "type": "number"
+ "type": "string"
},
{
- "type": "string",
- "const": "auto"
+ "$ref": "#/definitions/GradientDef"
}
],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
+ "description": "Default fill color or gradient."
},
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
+ "stroke": {
+ "type": "string",
+ "description": "Default stroke color."
+ },
+ "strokeWidth": {
+ "type": "number",
+ "description": "Default stroke width."
+ },
+ "tooltip": {
+ "type": [
+ "boolean",
+ "null"
],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
+ "description": "Tooltip behavior. null disables tooltips."
+ },
+ "clip": {
+ "type": "boolean",
+ "description": "Clip marks to the chart area."
+ },
+ "fillPattern": {
+ "type": "string",
+ "enum": [
+ "auto",
+ "none"
+ ],
+ "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
}
},
"required": [
@@ -3837,7 +1967,7 @@
]
},
"encoding": {
- "$ref": "#/definitions/TextEncoding%3CDataRow%3E"
+ "$ref": "#/definitions/HistogramEncoding%3CDataRow%3E"
}
}
}
@@ -3860,16 +1990,12 @@
"anyOf": [
{
"type": "string",
- "const": "tick"
+ "const": "density"
},
{
"type": "object",
"additionalProperties": false,
"properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
"point": {
"anyOf": [
{
@@ -3908,2178 +2034,39 @@
],
"description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
},
- "orient": {
- "type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
+ "bandwidth": {
+ "type": "number",
+ "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
},
- "innerRadius": {
+ "cumulative": {
+ "type": "boolean",
+ "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
+ },
+ "steps": {
"type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
+ "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
},
- "outerRadius": {
+ "type": {
+ "type": "string",
+ "const": "density"
+ },
+ "size": {
"type": "number",
- "description": "Outer radius for arc marks."
+ "description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
},
- "startAngle": {
+ "filled": {
+ "type": "boolean",
+ "description": "Whether the mark is filled (vs stroked only)."
+ },
+ "opacity": {
"type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
+ "description": "Default opacity (0-1)."
},
- "endAngle": {
+ "fillOpacity": {
"type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
+ "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
- },
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
- },
- "size": {
- "type": "number",
- "description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
- },
- "filled": {
- "type": "boolean",
- "description": "Whether the mark is filled (vs stroked only)."
- },
- "opacity": {
- "type": "number",
- "description": "Default opacity (0-1)."
- },
- "fillOpacity": {
- "type": "number",
- "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
- },
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
- "fill": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "$ref": "#/definitions/GradientDef"
- }
- ],
- "description": "Default fill color or gradient."
- },
- "stroke": {
- "type": "string",
- "description": "Default stroke color."
- },
- "strokeWidth": {
- "type": "number",
- "description": "Default stroke width."
- },
- "tooltip": {
- "type": [
- "boolean",
- "null"
- ],
- "description": "Tooltip behavior. null disables tooltips."
- },
- "clip": {
- "type": "boolean",
- "description": "Clip marks to the chart area."
- },
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
- "fillPattern": {
- "type": "string",
- "enum": [
- "auto",
- "none"
- ],
- "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
- }
- },
- "required": [
- "type"
- ]
- }
- ]
- },
- "encoding": {
- "$ref": "#/definitions/TickEncoding%3CDataRow%3E"
- }
- }
- }
- ]
- },
- {
- "allOf": [
- {
- "$ref": "#/definitions/ChartSpecBase"
- },
- {
- "type": "object",
- "required": [
- "data",
- "encoding",
- "mark"
- ],
- "properties": {
- "mark": {
- "anyOf": [
- {
- "type": "string",
- "const": "rect"
- },
- {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
- "type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
- },
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
- },
- "size": {
- "type": "number",
- "description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
- },
- "filled": {
- "type": "boolean",
- "description": "Whether the mark is filled (vs stroked only)."
- },
- "opacity": {
- "type": "number",
- "description": "Default opacity (0-1)."
- },
- "fillOpacity": {
- "type": "number",
- "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
- },
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
- "fill": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "$ref": "#/definitions/GradientDef"
- }
- ],
- "description": "Default fill color or gradient."
- },
- "stroke": {
- "type": "string",
- "description": "Default stroke color."
- },
- "strokeWidth": {
- "type": "number",
- "description": "Default stroke width."
- },
- "tooltip": {
- "type": [
- "boolean",
- "null"
- ],
- "description": "Tooltip behavior. null disables tooltips."
- },
- "clip": {
- "type": "boolean",
- "description": "Clip marks to the chart area."
- },
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
- "fillPattern": {
- "type": "string",
- "enum": [
- "auto",
- "none"
- ],
- "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
- }
- },
- "required": [
- "type"
- ]
- }
- ]
- },
- "encoding": {
- "$ref": "#/definitions/RectEncoding%3CDataRow%3E"
- }
- }
- }
- ]
- },
- {
- "allOf": [
- {
- "$ref": "#/definitions/ChartSpecBase"
- },
- {
- "type": "object",
- "required": [
- "data",
- "encoding",
- "mark"
- ],
- "properties": {
- "mark": {
- "anyOf": [
- {
- "type": "string",
- "const": "rule"
- },
- {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
- "type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
- },
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
- },
- "size": {
- "type": "number",
- "description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
- },
- "filled": {
- "type": "boolean",
- "description": "Whether the mark is filled (vs stroked only)."
- },
- "opacity": {
- "type": "number",
- "description": "Default opacity (0-1)."
- },
- "fillOpacity": {
- "type": "number",
- "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
- },
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
- "fill": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "$ref": "#/definitions/GradientDef"
- }
- ],
- "description": "Default fill color or gradient."
- },
- "stroke": {
- "type": "string",
- "description": "Default stroke color."
- },
- "strokeWidth": {
- "type": "number",
- "description": "Default stroke width."
- },
- "tooltip": {
- "type": [
- "boolean",
- "null"
- ],
- "description": "Tooltip behavior. null disables tooltips."
- },
- "clip": {
- "type": "boolean",
- "description": "Clip marks to the chart area."
- },
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
- "fillPattern": {
- "type": "string",
- "enum": [
- "auto",
- "none"
- ],
- "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
- }
- },
- "required": [
- "type"
- ]
- }
- ]
- },
- "encoding": {
- "$ref": "#/definitions/Encoding%3CDataRow%3E"
- }
- }
- }
- ]
- },
- {
- "allOf": [
- {
- "$ref": "#/definitions/ChartSpecBase"
- },
- {
- "type": "object",
- "required": [
- "data",
- "encoding",
- "mark"
- ],
- "properties": {
- "mark": {
- "anyOf": [
- {
- "type": "string",
- "const": "calendar"
- },
- {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
- "type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
- },
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
- },
- "size": {
- "type": "number",
- "description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
- },
- "filled": {
- "type": "boolean",
- "description": "Whether the mark is filled (vs stroked only)."
- },
- "opacity": {
- "type": "number",
- "description": "Default opacity (0-1)."
- },
- "fillOpacity": {
- "type": "number",
- "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
- },
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
- "fill": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "$ref": "#/definitions/GradientDef"
- }
- ],
- "description": "Default fill color or gradient."
- },
- "stroke": {
- "type": "string",
- "description": "Default stroke color."
- },
- "strokeWidth": {
- "type": "number",
- "description": "Default stroke width."
- },
- "tooltip": {
- "type": [
- "boolean",
- "null"
- ],
- "description": "Tooltip behavior. null disables tooltips."
- },
- "clip": {
- "type": "boolean",
- "description": "Clip marks to the chart area."
- },
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
- "fillPattern": {
- "type": "string",
- "enum": [
- "auto",
- "none"
- ],
- "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
- }
- },
- "required": [
- "type"
- ]
- }
- ]
- },
- "encoding": {
- "$ref": "#/definitions/CalendarEncoding%3CDataRow%3E"
- }
- }
- }
- ]
- },
- {
- "allOf": [
- {
- "$ref": "#/definitions/ChartSpecBase"
- },
- {
- "type": "object",
- "required": [
- "data",
- "encoding",
- "mark"
- ],
- "properties": {
- "mark": {
- "anyOf": [
- {
- "type": "string",
- "const": "parliament"
- },
- {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
- "type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
- },
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
- },
- "size": {
- "type": "number",
- "description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
- },
- "filled": {
- "type": "boolean",
- "description": "Whether the mark is filled (vs stroked only)."
- },
- "opacity": {
- "type": "number",
- "description": "Default opacity (0-1)."
- },
- "fillOpacity": {
- "type": "number",
- "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
- },
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
- "fill": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "$ref": "#/definitions/GradientDef"
- }
- ],
- "description": "Default fill color or gradient."
- },
- "stroke": {
- "type": "string",
- "description": "Default stroke color."
- },
- "strokeWidth": {
- "type": "number",
- "description": "Default stroke width."
- },
- "tooltip": {
- "type": [
- "boolean",
- "null"
- ],
- "description": "Tooltip behavior. null disables tooltips."
- },
- "clip": {
- "type": "boolean",
- "description": "Clip marks to the chart area."
- },
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
- "fillPattern": {
- "type": "string",
- "enum": [
- "auto",
- "none"
- ],
- "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
- }
- },
- "required": [
- "type"
- ]
- }
- ]
- },
- "encoding": {
- "$ref": "#/definitions/ParliamentEncoding%3CDataRow%3E"
- }
- }
- }
- ]
- },
- {
- "allOf": [
- {
- "$ref": "#/definitions/ChartSpecBase"
- },
- {
- "type": "object",
- "required": [
- "data",
- "encoding",
- "mark"
- ],
- "properties": {
- "mark": {
- "anyOf": [
- {
- "type": "string",
- "const": "histogram"
- },
- {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
- "type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
- },
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
- },
- "size": {
- "type": "number",
- "description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
- },
- "filled": {
- "type": "boolean",
- "description": "Whether the mark is filled (vs stroked only)."
- },
- "opacity": {
- "type": "number",
- "description": "Default opacity (0-1)."
- },
- "fillOpacity": {
- "type": "number",
- "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
- },
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
- "fill": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "$ref": "#/definitions/GradientDef"
- }
- ],
- "description": "Default fill color or gradient."
- },
- "stroke": {
- "type": "string",
- "description": "Default stroke color."
- },
- "strokeWidth": {
- "type": "number",
- "description": "Default stroke width."
- },
- "tooltip": {
- "type": [
- "boolean",
- "null"
- ],
- "description": "Tooltip behavior. null disables tooltips."
- },
- "clip": {
- "type": "boolean",
- "description": "Clip marks to the chart area."
- },
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
- "fillPattern": {
- "type": "string",
- "enum": [
- "auto",
- "none"
- ],
- "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
- }
- },
- "required": [
- "type"
- ]
- }
- ]
- },
- "encoding": {
- "$ref": "#/definitions/HistogramEncoding%3CDataRow%3E"
- }
- }
- }
- ]
- },
- {
- "allOf": [
- {
- "$ref": "#/definitions/ChartSpecBase"
- },
- {
- "type": "object",
- "required": [
- "data",
- "encoding",
- "mark"
- ],
- "properties": {
- "mark": {
- "anyOf": [
- {
- "type": "string",
- "const": "density"
- },
- {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
- "type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
- },
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
- },
- "size": {
- "type": "number",
- "description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
- },
- "filled": {
- "type": "boolean",
- "description": "Whether the mark is filled (vs stroked only)."
- },
- "opacity": {
- "type": "number",
- "description": "Default opacity (0-1)."
- },
- "fillOpacity": {
- "type": "number",
- "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
- },
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
- "fill": {
+ "fill": {
"anyOf": [
{
"type": "string"
@@ -6109,47 +2096,6 @@
"type": "boolean",
"description": "Clip marks to the chart area."
},
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
"fillPattern": {
"type": "string",
"enum": [
@@ -6157,99 +2103,6 @@
"none"
],
"description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
}
},
"required": [
@@ -6268,30 +2121,6 @@
],
"description": "Chart specification: the primary input for standard chart types.\n\nUses the Vega-Lite `mark` property instead of `type` to specify the visualization mark. The mark can be a string shorthand or an object with additional properties (interpolation, point markers, etc.).\n\nThis is a discriminated union — the `mark` value determines which encoding channels are required. TypeScript enforces required channels at compile time, matching the runtime rules in `MARK_ENCODING_RULES`."
},
- "MarkType": {
- "type": "string",
- "enum": [
- "bar",
- "line",
- "area",
- "point",
- "circle",
- "arc",
- "text",
- "rule",
- "tick",
- "rect",
- "lollipop",
- "beeswarm",
- "range",
- "waffle",
- "calendar",
- "parliament",
- "histogram",
- "density"
- ],
- "description": "Supported mark types, following Vega-Lite conventions.\n\nMapping from previous OpenChart chart types:\n- 'bar' covers both horizontal bars (old 'bar') and vertical columns (old 'column')\n- 'arc' covers both pie (old 'pie') and donut (old 'donut')\n- 'point' replaces old 'scatter'\n- 'circle' replaces old 'dot'\n- 'line' and 'area' unchanged\n\nNew mark types not in previous OpenChart:\n- 'text': data-positioned text labels\n- 'rule': reference lines as data marks\n- 'tick': strip/rug plot marks\n- 'rect': heatmaps and 2D binned plots\n- 'range': dumbbell / arrow / range-bar plots spanning two values per category\n- 'waffle': unit grids (\"x of 100\") for part-to-whole counts\n- 'calendar': GitHub-style calendar heatmap (weeks x weekdays, daily dates)\n- 'parliament': hemicycle seat-dot chart for election/legislature results\n- 'histogram': binned bars over a continuous field; sugar for a bar with `x.bin` + `y: { aggregate: 'count' }`\n- 'density': kernel density estimate drawn as a filled curve; sugar for an area over a density transform"
- },
"GradientDef": {
"anyOf": [
{
@@ -6392,46 +2221,23 @@
},
"x2": {
"type": "number",
- "description": "Outer circle center x (0-1). Default: 0.5."
- },
- "y2": {
- "type": "number",
- "description": "Outer circle center y (0-1). Default: 0.5."
- },
- "r2": {
- "type": "number",
- "description": "Outer circle radius (0-1). Default: 0.5."
- }
- },
- "required": [
- "gradient",
- "stops"
- ],
- "additionalProperties": false,
- "description": "Radial gradient definition. Coordinates are in [0,1] normalized space relative to the mark's bounding box."
- },
- "TrendlineConfig": {
- "type": "object",
- "properties": {
- "layer": {
- "type": "string",
- "enum": [
- "above",
- "below"
- ],
- "description": "Stacking order relative to the point marks. `'below'` (default) draws the line behind the dots; `'above'` draws it on top so it stays visible over a dense cloud."
- },
- "stroke": {
- "type": "string",
- "description": "Stroke color. Defaults to the theme's high-contrast text color so the line reads on both light and dark grounds. Set an explicit CSS color to override."
+ "description": "Outer circle center x (0-1). Default: 0.5."
},
- "strokeWidth": {
+ "y2": {
"type": "number",
- "description": "Stroke width in pixels. Defaults to 1.5. Over a dense cloud with `layer: 'above'`, a heavier line (e.g. 2.5) stays legible where the thin default disappears into the dots."
+ "description": "Outer circle center y (0-1). Default: 0.5."
+ },
+ "r2": {
+ "type": "number",
+ "description": "Outer circle radius (0-1). Default: 0.5."
}
},
+ "required": [
+ "gradient",
+ "stops"
+ ],
"additionalProperties": false,
- "description": "Configuration for the scatter regression trend line. Passed as the object form of `MarkDef.trendline`."
+ "description": "Radial gradient definition. Coordinates are in [0,1] normalized space relative to the mark's bounding box."
},
"ArcEncoding": {
"type": "object",
@@ -6547,7 +2353,7 @@
"color"
],
"additionalProperties": false,
- "description": "Encoding for arc marks (pie/donut charts).\n- `theta`: required (quantitative — the canonical slice value channel)\n- `color`: required (nominal/ordinal — the category)\n- `y`: deprecated alias for `theta`, accepted for backward compatibility"
+ "description": "Encoding for arc marks (pie/donut charts).\n- `theta`: required (quantitative — the canonical slice value channel)\n- `color`: required (nominal/ordinal — the category)"
},
"EncodingChannel": {
"type": "object",
@@ -6800,6 +2606,19 @@
"type": "number",
"description": "Axis position offset in pixels."
},
+ "titleAngle": {
+ "type": "number",
+ "description": "Angle of the title in degrees. A y-axis title at angle 0 and anchor end becomes a header."
+ },
+ "titleAnchor": {
+ "type": "string",
+ "enum": [
+ "start",
+ "middle",
+ "end"
+ ],
+ "description": "Position along the axis for the title; omission keeps the centered title."
+ },
"titlePadding": {
"type": "number",
"description": "Padding between axis title and axis."
@@ -8097,10 +3916,6 @@
"type": "string",
"description": "Stable identifier for selection and edit callbacks. When provided, edit events include this ID for reliable element matching."
},
- "label": {
- "type": "string",
- "description": "Human-readable label for the annotation."
- },
"fill": {
"type": "string",
"description": "Fill color for the annotation element."
@@ -8149,18 +3964,18 @@
},
"subtitle": {
"type": "string",
- "description": "Optional muted second-tone text rendered below the primary `text`. Used for supporting context (e.g. methodology, source). Newlines in `text` still produce multi-line primary; subtitle is a separate block.\n\nSupports the same inline `**bold**` spans as `text`. When a subtitle is present and no `fontWeight` is set, the primary `text` resolves to weight 700 — the lede + context stack."
+ "description": "Optional muted second-tone text rendered below the primary `text`. Used for supporting context (e.g. methodology, source). Newlines in `text` still produce multi-line primary; subtitle is a separate block.\n\nSupports the same inline `**bold**` spans as `text`. The primary `text` keeps its own weight (default 400); use `fontWeight` or `**bold**` spans for a bold lede."
},
"subtitleFontSize": {
"type": "number",
- "description": "Subtitle size in pixels. Default: round(fontSize * 0.85)."
+ "description": "Subtitle size in pixels. Default: round(fontSize * 12 / 13)."
},
"maxWidth": {
"type": [
"number",
"null"
],
- "description": "Optional display wrap width in pixels. Omitted or null keeps unbounded lines."
+ "description": "Display wrap width in pixels. Omitted uses min(240, plot width minus twice the padding); null disables wrapping."
},
"dot": {
"anyOf": [
@@ -8201,7 +4016,7 @@
"$ref": "#/definitions/ConnectorConfig"
}
],
- "description": "Connector from label to anchor point.\n- `true` (default): straight line, no arrow\n- `'straight'`: straight line, no arrow\n- `'curve'`: curved arrow with arrowhead\n- `'drop-line'`: vertical line through the data point's x\n- `{ type, arrow? }`: object form for explicit arrow control\n- `false`: no connector\n\nThe connector leaves the text block on the side facing the data point, with a small standoff gap. A leader shorter than 8px is dropped as noise (the marker alone reads better), as is a connector whose target sits inside the text block. Widen `offset` if you want the line back."
+ "description": "Connector from label to anchor point.\n- `true` (default): straight line, no arrow\n- `'straight'`: straight line, no arrow\n- `'curve'`: curved leader; set arrow: true for an arrowhead\n- `'drop-line'`: vertical line through the data point's x\n- `{ type, arrow? }`: object form for explicit arrow control\n- `false`: no connector\n\nThe connector leaves the text block on the side facing the data point, with a small standoff gap. A leader shorter than 8px is dropped as noise (the marker alone reads better), as is a connector whose target sits inside the text block. Widen `offset` if you want the line back."
},
"connectorOffset": {
"type": "object",
@@ -8253,7 +4068,7 @@
},
"fill": {
"type": "string",
- "description": "Fill color. Defaults to theme background for an \"open ring\" look."
+ "description": "Fill color. Defaults to `theme.colors.annotationMarkerFill`, which resolves to the theme's opaque surface. A default dot on an endpoint marker takes the marker's fill and stroke."
},
"stroke": {
"type": "string",
@@ -8300,7 +4115,7 @@
},
"arrow": {
"type": "boolean",
- "description": "Whether to draw an arrowhead at the data-point end. Default: true for curve, false for straight. Drop-line never takes a head."
+ "description": "Whether to draw an arrowhead at the data-point end. Default: false for both curve and straight. Drop-line never takes a head."
}
},
"required": [
@@ -8316,10 +4131,6 @@
"type": "string",
"description": "Stable identifier for selection and edit callbacks. When provided, edit events include this ID for reliable element matching."
},
- "label": {
- "type": "string",
- "description": "Human-readable label for the annotation."
- },
"fill": {
"type": "string",
"description": "Fill color for the annotation element."
@@ -8344,6 +4155,10 @@
"type": "string",
"const": "range"
},
+ "label": {
+ "type": "string",
+ "description": "Text drawn with the range band."
+ },
"x1": {
"type": [
"string",
@@ -8386,7 +4201,7 @@
},
"fontSize": {
"type": "number",
- "description": "Font size override for the range label (px). Default: 11."
+ "description": "Font size override for the range label (px). Default: theme small (12px)."
},
"fontWeight": {
"type": "number",
@@ -8406,10 +4221,6 @@
"type": "string",
"description": "Stable identifier for selection and edit callbacks. When provided, edit events include this ID for reliable element matching."
},
- "label": {
- "type": "string",
- "description": "Human-readable label for the annotation."
- },
"fill": {
"type": "string",
"description": "Fill color for the annotation element."
@@ -8433,7 +4244,11 @@
"type": {
"type": "string",
"const": "refline",
- "description": "Discriminant. `'refline'` is canonical and the only value in the type. `'rule'` (it collides with the `rule` mark type) is still accepted at runtime for backward compatibility -- the spec-sugar pass rewrites it to `'refline'` with a deprecation warning before validation -- but it is not part of this type. Use `'refline'`."
+ "description": "Reference-line annotation discriminant. Older rule aliases require migrateSpecV8."
+ },
+ "label": {
+ "type": "string",
+ "description": "Text drawn with the reference line."
},
"x": {
"type": [
@@ -8481,7 +4296,7 @@
"end",
"outside"
],
- "description": "Opt-in screen-space label placement. Outside reserves a right gutter or top lane; omission preserves legacy labelAnchor behavior."
+ "description": "Screen-space label placement. Outside reserves a right gutter or top lane; omission uses auto unless labelAnchor is authored."
},
"labelAnchor": {
"$ref": "#/definitions/AnnotationAnchor",
@@ -8489,7 +4304,7 @@
},
"fontSize": {
"type": "number",
- "description": "Label font size in pixels. Default: 11."
+ "description": "Label font size in pixels. Default: theme small (12px)."
},
"fontWeight": {
"type": "number",
@@ -8516,6 +4331,16 @@
"LabelConfig": {
"type": "object",
"properties": {
+ "placement": {
+ "type": "string",
+ "enum": [
+ "adaptive",
+ "inside",
+ "outside",
+ "column"
+ ],
+ "description": "Placement for bar/column values. Column is only supported for simple horizontal bars."
+ },
"density": {
"$ref": "#/definitions/LabelDensity",
"description": "How many labels to show. Defaults to 'auto'."
@@ -8536,14 +4361,6 @@
"type": "string",
"description": "Fixed CSS color for all labels. Overrides the default fill-derived color."
},
- "offsets": {
- "type": "object",
- "additionalProperties": {
- "$ref": "#/definitions/AnnotationOffset"
- },
- "description": "Per-series pixel offsets for fine-tuning label positions, keyed by series name.",
- "deprecated": "Per-series pixel offsets are a workaround for collision bugs\nnow fixed by the layout engine. Will be removed in a future version."
- },
"fontSize": {
"type": "number",
"description": "Font size in pixels for bar/column value labels."
@@ -8560,11 +4377,27 @@
"endpoints",
"none"
],
- "description": "Label density mode controlling how many data labels are shown.\n- 'all': show every label, skip collision detection\n- 'auto': show labels with collision detection (default)\n- 'endpoints': show only first and last per series (useful for line charts)\n- 'none': hide all labels (rely on tooltips and legend)"
+ "description": "Label density mode controlling how many data labels are shown.\n- 'all': show every label, skip collision detection\n- 'auto': show labels with collision detection (default)\n- 'endpoints': show only first and last per series\n\nLine and area marks draw no value labels; identify their series with `endpointLabels` or the legend.\n- 'none': hide all labels (rely on tooltips and legend)"
},
"EndpointLabelsConfig": {
"type": "object",
"properties": {
+ "anchor": {
+ "type": "string",
+ "enum": [
+ "boundary",
+ "band"
+ ],
+ "description": "Stacked-area anchor: cumulative boundary or the middle of the band. Default band."
+ },
+ "placement": {
+ "type": "string",
+ "enum": [
+ "end",
+ "interior"
+ ],
+ "description": "Series identification in the end column or fitted inside stacked bands."
+ },
"show": {
"type": "boolean",
"description": "Explicit on/off. When undefined, the chart auto-decides based on series count."
@@ -8615,7 +4448,7 @@
},
"showMarker": {
"type": "boolean",
- "description": "Render an open-circle marker on the line at the right edge. Default true."
+ "description": "Render a ring marker centered on the line's end point. Default true. Its fill defaults to the opaque canvas color so the ring covers the line end."
},
"markerStyle": {
"type": "object",
@@ -8634,7 +4467,7 @@
}
},
"additionalProperties": false,
- "description": "Override the marker style."
+ "description": "Override the marker style. `fill: 'transparent'` keeps a see-through ring."
},
"showLeader": {
"type": "boolean",
@@ -8642,7 +4475,7 @@
}
},
"additionalProperties": false,
- "description": "Configuration for the endpoint labels column rendered at the chart's right edge for multi-series line/area charts. Each entry pairs the series name with its last formatted value, optionally anchored to the line by an open-circle marker.\n\nThe column is independent of the traditional `legend` and the legacy end-of-line labels. Together with `legend.show`, the three suppression toggles follow this truth table for ≥2-series line/area charts:\n\n| `legend.show` | `endpointLabels` | Traditional legend | Endpoint column | End-of-line labels | |--|--|--|--|--| | unset | unset | hidden (auto-suppressed) | shown (default) | hidden | | true | unset | shown | shown | hidden | | unset | false | shown (auto-suppress revoked) | hidden | hidden | | false | false | hidden | hidden | shown (last-resort) | | true | false | shown | hidden | hidden | | false | true | hidden | shown | hidden | | true | true | shown | shown | hidden |\n\nSingle-series charts: column is hidden by default (nothing to identify).\n\nThe implementation is in `packages/engine/src/legend/suppression.ts` — `resolveSuppression` is the single source of truth. The table above is a user-facing mirror; if the two ever diverge, the engine wins. Tests in `legend/__tests__/suppression.test.ts` enforce every cell."
+ "description": "Configuration for the endpoint labels column rendered at the chart's right edge for multi-series line/area charts. Each entry pairs the series name with its last formatted value, optionally anchored to the line by an open-circle marker.\n\nThe column and traditional legend are independent identification surfaces. For multi-series line/area charts:\n\n| `legend.show` | `endpointLabels` | Traditional legend | Endpoint column | |--|--|--|--| | unset | unset | hidden | shown | | true | unset | shown | shown | | unset | false | shown | hidden | | false | false | hidden (accessibility warning) | hidden | | true | false | shown | hidden | | false | true | hidden | shown | | true | true | shown | shown |\n\nSingle-series charts: column is hidden by default (nothing to identify).\n\nThe implementation is in `packages/engine/src/legend/suppression.ts` — `resolveSuppression` is the single source of truth. The table above is a user-facing mirror; if the two ever diverge, the engine wins. Tests in `legend/__tests__/suppression.test.ts` enforce every cell."
},
"EndpointLabelContent": {
"type": "string",
@@ -8661,7 +4494,7 @@
"number",
"null"
],
- "description": "Maximum eligible inline callouts below 400px container width; null or omitted is unlimited. Pinned annotations are exempt."
+ "description": "Maximum eligible inline callouts below 400px container width; default 1; null is unlimited. Pinned annotations are exempt."
},
"preserveEnd": {
"type": "boolean",
@@ -8783,6 +4616,22 @@
"$ref": "#/definitions/TokenValue",
"description": "Annotation plate color. Accepts a TokenValue light/dark pair."
},
+ "gridlineZero": {
+ "$ref": "#/definitions/TokenValue",
+ "description": "Optional map missing-data and geographic border ink."
+ },
+ "mapNoDataFill": {
+ "$ref": "#/definitions/TokenValue"
+ },
+ "mapNoDataHatch": {
+ "$ref": "#/definitions/TokenValue"
+ },
+ "mapInteriorBorder": {
+ "$ref": "#/definitions/TokenValue"
+ },
+ "mapOutlineBorder": {
+ "$ref": "#/definitions/TokenValue"
+ },
"positive": {
"$ref": "#/definitions/TokenValue",
"description": "Semantic color for positive/up-trend values. Accepts a TokenValue for light/dark pairs."
@@ -8825,11 +4674,11 @@
},
"small": {
"type": "number",
- "description": "Small text (source line, footer). Default: 11."
+ "description": "Small text (source line, footer). Default: 12."
},
"axisTick": {
"type": "number",
- "description": "Axis tick labels. Default: 11."
+ "description": "Axis tick labels. Default: 12."
},
"metricLabel": {
"type": "number",
@@ -8838,6 +4687,13 @@
"metricValue": {
"type": "number",
"description": "KPI metric primary value. Default: 22. Delta/secondary derive from this."
+ },
+ "endpointLabel": {
+ "type": "number",
+ "description": "Series-name and numerical endpoint font sizes in pixels."
+ },
+ "endpointValue": {
+ "type": "number"
}
},
"additionalProperties": false,
@@ -8900,6 +4756,16 @@
"xAxisLabelPadding": {
"type": "number",
"description": "Gap in pixels between the x-axis line and tick label text. Increase when larger axisTick fonts sit too close to the axis line."
+ },
+ "endpointEntryGap": {
+ "type": "number",
+ "description": "Endpoint block separation, internal value gap, and plot-to-column gap."
+ },
+ "endpointValueGap": {
+ "type": "number"
+ },
+ "endpointColumnGap": {
+ "type": "number"
}
},
"additionalProperties": false,
@@ -8913,97 +4779,57 @@
"type": "object",
"properties": {
"eyebrow": {
- "anyOf": [
- {
- "$ref": "#/definitions/ChromeThemeOverride"
- },
- {
- "type": "string"
- }
- ],
+ "$ref": "#/definitions/ChromeThemeOverride",
"description": "Eyebrow (kicker) style override."
},
"title": {
- "anyOf": [
- {
- "$ref": "#/definitions/ChromeThemeOverride"
- },
- {
- "type": "string"
- }
- ],
+ "$ref": "#/definitions/ChromeThemeOverride",
"description": "Title style override."
},
"subtitle": {
- "anyOf": [
- {
- "$ref": "#/definitions/ChromeThemeOverride"
- },
- {
- "type": "string"
- }
- ],
+ "$ref": "#/definitions/ChromeThemeOverride",
"description": "Subtitle style override."
},
"source": {
- "anyOf": [
- {
- "$ref": "#/definitions/ChromeThemeOverride"
- },
- {
- "type": "string"
- }
- ],
+ "$ref": "#/definitions/ChromeThemeOverride",
"description": "Source/attribution style override."
},
"byline": {
- "anyOf": [
- {
- "$ref": "#/definitions/ChromeThemeOverride"
- },
- {
- "type": "string"
- }
- ],
+ "$ref": "#/definitions/ChromeThemeOverride",
"description": "Byline style override."
},
"footer": {
- "anyOf": [
- {
- "$ref": "#/definitions/ChromeThemeOverride"
- },
- {
- "type": "string"
- }
- ],
+ "$ref": "#/definitions/ChromeThemeOverride",
"description": "Footer style override."
}
},
"additionalProperties": false,
- "description": "Per-element chrome style overrides. Pass a plain string for color-only (backward-compatible), or a ChromeThemeOverride object for full typography control (color, fontSize, fontWeight, lineHeight)."
+ "description": "Per-element chrome typography control (color, fontSize, fontWeight, lineHeight)."
},
"rule": {
- "type": "object",
- "properties": {
- "color": {
- "$ref": "#/definitions/TokenValue",
- "description": "Rule color. Accepts a TokenValue for light/dark pairs."
- },
- "width": {
- "type": "number",
- "description": "Rule length in pixels."
+ "anyOf": [
+ {
+ "type": "object",
+ "properties": {
+ "color": {
+ "$ref": "#/definitions/TokenValue",
+ "description": "Rule color. Accepts a TokenValue for light/dark pairs."
+ },
+ "width": {
+ "type": "number",
+ "description": "Rule length in pixels."
+ },
+ "thickness": {
+ "type": "number",
+ "description": "Rule height in pixels."
+ }
+ },
+ "additionalProperties": false
},
- "thickness": {
- "type": "number",
- "description": "Rule height in pixels."
+ {
+ "type": "null"
}
- },
- "required": [
- "color",
- "width",
- "thickness"
],
- "additionalProperties": false,
"description": "Editorial rule above the chrome block: a short colored bar (the Economist/FT masthead device). Omit for no rule."
},
"seriesStrategy": {
@@ -9059,7 +4885,7 @@
}
},
"additionalProperties": false,
- "description": "Per-element chrome theme override. Pass a plain string (color only, backward-compatible) or an object with full typography control."
+ "description": "Per-element chrome theme typography controls."
},
"SeriesStrategy": {
"type": "string",
@@ -9069,15 +4895,6 @@
],
"description": "Series color assignment strategy.\n\n- `'palette'` — always assign from the full categorical palette (current default, zero visual drift).\n- `'accent-neutral'` — assign by series count: a single series gets the accent (first categorical color) only; 2-4 series get accent for the first and neutral grays for the rest; 5+ fall back to the full palette."
},
- "DarkMode": {
- "type": "string",
- "enum": [
- "auto",
- "force",
- "off"
- ],
- "description": "Dark mode behavior.\n\n- `\"auto\"` - Checks the `prefers-color-scheme` media query to detect the user's system-level preference. This does NOT detect class-based dark mode toggles (e.g. `document.documentElement.classList.toggle('dark')`). If your app uses class-based dark mode, use VizThemeProvider with `\"force\"` or `\"off\"` instead of `\"auto\"` and toggle based on your app's state.\n- `\"force\"` - Always render in dark mode regardless of system preference.\n- `\"off\"` - Always render in light mode (default).\n\nAll components (Chart, Sankey, Graph) inherit darkMode from VizThemeProvider when no explicit prop is passed."
- },
"SeriesStyle": {
"type": "object",
"properties": {
@@ -9117,6 +4934,17 @@
"$ref": "#/definitions/LabelSpec",
"description": "Override label configuration at this breakpoint."
},
+ "endpointLabels": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "$ref": "#/definitions/EndpointLabelsConfig"
+ }
+ ],
+ "description": "Override endpoint labels at this breakpoint, including migrated v8 line labels."
+ },
"legend": {
"$ref": "#/definitions/LegendConfig",
"description": "Override legend configuration at this breakpoint."
@@ -9166,8 +4994,7 @@
"type": "string",
"enum": [
"fluid",
- "subtle",
- "legacy"
+ "subtle"
],
"description": "Opt-in motion language."
},
@@ -9552,7 +5379,7 @@
},
"theta": {
"$ref": "#/definitions/EncodingChannel%3CDataRow%3E",
- "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). `y` is still accepted on these marks as a deprecated alias for backward compatibility (a compile warning is emitted and `y` is populated from `theta` internally). Not used by any other mark type."
+ "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). The compiler populates internal `y` from `theta`; authored `y` aliases are rejected. Not used by any other mark type."
},
"facet": {
"$ref": "#/definitions/FacetChannel%3CDataRow%3E",
@@ -9671,7 +5498,7 @@
},
"theta": {
"$ref": "#/definitions/EncodingChannel%3CDataRow%3E",
- "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). `y` is still accepted on these marks as a deprecated alias for backward compatibility (a compile warning is emitted and `y` is populated from `theta` internally). Not used by any other mark type."
+ "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). The compiler populates internal `y` from `theta`; authored `y` aliases are rejected. Not used by any other mark type."
},
"facet": {
"$ref": "#/definitions/FacetChannel%3CDataRow%3E",
@@ -9790,7 +5617,7 @@
},
"theta": {
"$ref": "#/definitions/EncodingChannel%3CDataRow%3E",
- "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). `y` is still accepted on these marks as a deprecated alias for backward compatibility (a compile warning is emitted and `y` is populated from `theta` internally). Not used by any other mark type."
+ "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). The compiler populates internal `y` from `theta`; authored `y` aliases are rejected. Not used by any other mark type."
},
"facet": {
"$ref": "#/definitions/FacetChannel%3CDataRow%3E",
@@ -9812,6 +5639,29 @@
"additionalProperties": false,
"description": "Encoding for area marks.\n- `x`: required (temporal or ordinal)\n- `y`: required (quantitative)"
},
+ "TrendlineConfig": {
+ "type": "object",
+ "properties": {
+ "layer": {
+ "type": "string",
+ "enum": [
+ "above",
+ "below"
+ ],
+ "description": "Stacking order relative to the point marks. `'below'` (default) draws the line behind the dots; `'above'` draws it on top so it stays visible over a dense cloud."
+ },
+ "stroke": {
+ "type": "string",
+ "description": "Stroke color. Defaults to the theme's high-contrast text color so the line reads on both light and dark grounds. Set an explicit CSS color to override."
+ },
+ "strokeWidth": {
+ "type": "number",
+ "description": "Stroke width in pixels. Defaults to 1.5. Over a dense cloud with `layer: 'above'`, a heavier line (e.g. 2.5) stays legible where the thin default disappears into the dots."
+ }
+ },
+ "additionalProperties": false,
+ "description": "Configuration for the scatter regression trend line. Passed as the object form of `MarkDef.trendline`."
+ },
"PointEncoding": {
"type": "object",
"properties": {
@@ -9909,7 +5759,7 @@
},
"theta": {
"$ref": "#/definitions/EncodingChannel%3CDataRow%3E",
- "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). `y` is still accepted on these marks as a deprecated alias for backward compatibility (a compile warning is emitted and `y` is populated from `theta` internally). Not used by any other mark type."
+ "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). The compiler populates internal `y` from `theta`; authored `y` aliases are rejected. Not used by any other mark type."
},
"facet": {
"$ref": "#/definitions/FacetChannel%3CDataRow%3E",
@@ -10028,7 +5878,7 @@
},
"theta": {
"$ref": "#/definitions/EncodingChannel%3CDataRow%3E",
- "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). `y` is still accepted on these marks as a deprecated alias for backward compatibility (a compile warning is emitted and `y` is populated from `theta` internally). Not used by any other mark type."
+ "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). The compiler populates internal `y` from `theta`; authored `y` aliases are rejected. Not used by any other mark type."
},
"facet": {
"$ref": "#/definitions/FacetChannel%3CDataRow%3E",
@@ -10147,7 +5997,7 @@
},
"theta": {
"$ref": "#/definitions/EncodingChannel%3CDataRow%3E",
- "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). `y` is still accepted on these marks as a deprecated alias for backward compatibility (a compile warning is emitted and `y` is populated from `theta` internally). Not used by any other mark type."
+ "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). The compiler populates internal `y` from `theta`; authored `y` aliases are rejected. Not used by any other mark type."
},
"facet": {
"$ref": "#/definitions/FacetChannel%3CDataRow%3E",
@@ -10266,7 +6116,7 @@
},
"theta": {
"$ref": "#/definitions/EncodingChannel%3CDataRow%3E",
- "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). `y` is still accepted on these marks as a deprecated alias for backward compatibility (a compile warning is emitted and `y` is populated from `theta` internally). Not used by any other mark type."
+ "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). The compiler populates internal `y` from `theta`; authored `y` aliases are rejected. Not used by any other mark type."
},
"facet": {
"$ref": "#/definitions/FacetChannel%3CDataRow%3E",
@@ -10381,7 +6231,7 @@
},
"theta": {
"$ref": "#/definitions/EncodingChannel%3CDataRow%3E",
- "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). `y` is still accepted on these marks as a deprecated alias for backward compatibility (a compile warning is emitted and `y` is populated from `theta` internally). Not used by any other mark type."
+ "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). The compiler populates internal `y` from `theta`; authored `y` aliases are rejected. Not used by any other mark type."
},
"facet": {
"$ref": "#/definitions/FacetChannel%3CDataRow%3E",
@@ -10497,7 +6347,7 @@
},
"theta": {
"$ref": "#/definitions/EncodingChannel%3CDataRow%3E",
- "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). `y` is still accepted on these marks as a deprecated alias for backward compatibility (a compile warning is emitted and `y` is populated from `theta` internally). Not used by any other mark type."
+ "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). The compiler populates internal `y` from `theta`; authored `y` aliases are rejected. Not used by any other mark type."
},
"facet": {
"$ref": "#/definitions/FacetChannel%3CDataRow%3E",
@@ -10516,7 +6366,7 @@
"color"
],
"additionalProperties": false,
- "description": "Encoding for waffle marks (unit grids for part-to-whole counts).\n- `color`: required (nominal/ordinal, the category)\n- `theta`: the canonical quantitative share, the same part-to-whole value channel arc marks use. `y` is a deprecated alias, accepted for backward compatibility."
+ "description": "Encoding for waffle marks (unit grids for part-to-whole counts).\n- `color`: required (nominal/ordinal, the category)\n- `theta`: the canonical quantitative share, the same part-to-whole value channel arc marks use."
},
"TextEncoding": {
"type": "object",
@@ -10615,7 +6465,7 @@
},
"theta": {
"$ref": "#/definitions/EncodingChannel%3CDataRow%3E",
- "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). `y` is still accepted on these marks as a deprecated alias for backward compatibility (a compile warning is emitted and `y` is populated from `theta` internally). Not used by any other mark type."
+ "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). The compiler populates internal `y` from `theta`; authored `y` aliases are rejected. Not used by any other mark type."
},
"facet": {
"$ref": "#/definitions/FacetChannel%3CDataRow%3E",
@@ -10733,7 +6583,7 @@
},
"theta": {
"$ref": "#/definitions/EncodingChannel%3CDataRow%3E",
- "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). `y` is still accepted on these marks as a deprecated alias for backward compatibility (a compile warning is emitted and `y` is populated from `theta` internally). Not used by any other mark type."
+ "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). The compiler populates internal `y` from `theta`; authored `y` aliases are rejected. Not used by any other mark type."
},
"facet": {
"$ref": "#/definitions/FacetChannel%3CDataRow%3E",
@@ -10852,7 +6702,7 @@
},
"theta": {
"$ref": "#/definitions/EncodingChannel%3CDataRow%3E",
- "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). `y` is still accepted on these marks as a deprecated alias for backward compatibility (a compile warning is emitted and `y` is populated from `theta` internally). Not used by any other mark type."
+ "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). The compiler populates internal `y` from `theta`; authored `y` aliases are rejected. Not used by any other mark type."
},
"facet": {
"$ref": "#/definitions/FacetChannel%3CDataRow%3E",
@@ -10971,7 +6821,7 @@
},
"theta": {
"$ref": "#/definitions/EncodingChannel%3CDataRow%3E",
- "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). `y` is still accepted on these marks as a deprecated alias for backward compatibility (a compile warning is emitted and `y` is populated from `theta` internally). Not used by any other mark type."
+ "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). The compiler populates internal `y` from `theta`; authored `y` aliases are rejected. Not used by any other mark type."
},
"facet": {
"$ref": "#/definitions/FacetChannel%3CDataRow%3E",
@@ -11076,7 +6926,7 @@
},
"theta": {
"$ref": "#/definitions/EncodingChannel%3CDataRow%3E",
- "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). `y` is still accepted on these marks as a deprecated alias for backward compatibility (a compile warning is emitted and `y` is populated from `theta` internally). Not used by any other mark type."
+ "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). The compiler populates internal `y` from `theta`; authored `y` aliases are rejected. Not used by any other mark type."
},
"facet": {
"$ref": "#/definitions/FacetChannel%3CDataRow%3E",
@@ -11192,7 +7042,7 @@
},
"theta": {
"$ref": "#/definitions/EncodingChannel%3CDataRow%3E",
- "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). `y` is still accepted on these marks as a deprecated alias for backward compatibility (a compile warning is emitted and `y` is populated from `theta` internally). Not used by any other mark type."
+ "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). The compiler populates internal `y` from `theta`; authored `y` aliases are rejected. Not used by any other mark type."
},
"facet": {
"$ref": "#/definitions/FacetChannel%3CDataRow%3E",
@@ -11211,7 +7061,7 @@
"color"
],
"additionalProperties": false,
- "description": "Encoding for parliament marks (hemicycle seat-dot charts).\n- `color`: required (nominal/ordinal, the party)\n- `theta`: the canonical quantitative seat count, the same part-to-whole value channel arc/waffle marks use. `y` is a deprecated alias, accepted for backward compatibility."
+ "description": "Encoding for parliament marks (hemicycle seat-dot charts).\n- `color`: required (nominal/ordinal, the party)\n- `theta`: the canonical quantitative seat count, the same part-to-whole value channel arc/waffle marks use."
},
"HistogramEncoding": {
"type": "object",
@@ -11310,7 +7160,7 @@
},
"theta": {
"$ref": "#/definitions/EncodingChannel%3CDataRow%3E",
- "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). `y` is still accepted on these marks as a deprecated alias for backward compatibility (a compile warning is emitted and `y` is populated from `theta` internally). Not used by any other mark type."
+ "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). The compiler populates internal `y` from `theta`; authored `y` aliases are rejected. Not used by any other mark type."
},
"facet": {
"$ref": "#/definitions/FacetChannel%3CDataRow%3E",
@@ -11428,7 +7278,7 @@
},
"theta": {
"$ref": "#/definitions/EncodingChannel%3CDataRow%3E",
- "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). `y` is still accepted on these marks as a deprecated alias for backward compatibility (a compile warning is emitted and `y` is populated from `theta` internally). Not used by any other mark type."
+ "description": "Angular position (slice value) for arc marks (pie/donut), the canonical value channel in v8. Waffle and parliament marks accept it too (the same part-to-whole value channel). The compiler populates internal `y` from `theta`; authored `y` aliases are rejected. Not used by any other mark type."
},
"facet": {
"$ref": "#/definitions/FacetChannel%3CDataRow%3E",
@@ -11543,17 +7393,20 @@
"description": "Legend display configuration."
},
"responsive": {
- "type": "boolean",
- "description": "Whether the chart adapts to container width. Defaults to true."
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "$ref": "#/definitions/ResponsiveConfig"
+ }
+ ],
+ "description": "Responsive behavior for the composed chart, with the same meaning as `ChartSpec.responsive`: `true`/`false` enables/disables container-width adaptation, and a `ResponsiveConfig` object gives fine-grained control (e.g. `{ compactAnnotationLimit: null }`). When omitted, the first leaf layer's `responsive` applies."
},
"theme": {
"$ref": "#/definitions/ThemeConfig",
"description": "Theme configuration overrides."
},
- "darkMode": {
- "$ref": "#/definitions/DarkMode",
- "deprecated": "Use the mount darkMode option; the spec field has no effect."
- },
"watermark": {
"type": "boolean",
"description": "Whether to show the OpenData watermark. Defaults to true."
@@ -11585,78 +7438,28 @@
"description": "Animation configuration."
}
},
- "required": [
- "layer"
- ],
- "additionalProperties": false,
- "description": "Layer specification: composites multiple chart layers into a single view.\n\nEach element in `layer` is either a ChartSpec or another LayerSpec (nested). Shared data, encoding, and transforms at the LayerSpec level are inherited by children that don't define their own."
- },
- "ChartSpec": {
- "anyOf": [
- {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "mark": {
- "anyOf": [
- {
- "type": "string",
- "const": "arc"
- },
- {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
- "type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
+ "required": [
+ "layer"
+ ],
+ "additionalProperties": false,
+ "description": "Layer specification: composites multiple chart layers into a single view.\n\nEach element in `layer` is either a ChartSpec or another LayerSpec (nested). Shared data, encoding, and transforms at the LayerSpec level are inherited by children that don't define their own."
+ },
+ "ChartSpec": {
+ "anyOf": [
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "mark": {
+ "anyOf": [
+ {
+ "type": "string",
+ "const": "arc"
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
"innerRadius": {
"type": "number",
"description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
@@ -11696,17 +7499,9 @@
],
"description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
},
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
+ "type": {
+ "type": "string",
+ "const": "arc"
},
"size": {
"type": "number",
@@ -11724,26 +7519,6 @@
"type": "number",
"description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
"fill": {
"anyOf": [
{
@@ -11774,47 +7549,6 @@
"type": "boolean",
"description": "Clip marks to the chart area."
},
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
"fillPattern": {
"type": "string",
"enum": [
@@ -11822,99 +7556,6 @@
"none"
],
"description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
}
},
"required": [
@@ -11957,7 +7598,7 @@
"subtract",
"grow"
],
- "description": "How chrome height interacts with the container height budget.\n\n- `'subtract'` (default): the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\n`'grow'` is a no-op for faceted specs in this version."
+ "description": "How chrome height interacts with the container height budget.\n\n- omitted: only notes and the series summary grow the figure; title, subtitle, and source subtract from the height budget.\n- `'subtract'`: the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\nFacets and layers report engine-owned growth once."
},
"title": {
"anyOf": [
@@ -12037,7 +7678,7 @@
"$ref": "#/definitions/EndpointLabelsConfig"
}
],
- "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show` and the legacy end-of-line labels."
+ "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show`."
},
"responsive": {
"anyOf": [
@@ -12054,10 +7695,6 @@
"$ref": "#/definitions/ThemeConfig",
"description": "Theme configuration overrides."
},
- "darkMode": {
- "$ref": "#/definitions/DarkMode",
- "deprecated": "Use the mount darkMode option; the spec field has no effect."
- },
"watermark": {
"type": "boolean",
"description": "Whether to show the OpenData watermark. Defaults to true; auto-hides when the container is under 200px tall (the cramped height class) unless explicitly set."
@@ -12148,10 +7785,6 @@
"type": "object",
"additionalProperties": false,
"properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
"point": {
"anyOf": [
{
@@ -12183,71 +7816,16 @@
"monotone",
"step",
"step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
- "type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
- },
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
+ "step-after",
+ "basis",
+ "cardinal",
+ "natural"
],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
+ "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
+ },
+ "type": {
+ "type": "string",
+ "const": "line"
},
"size": {
"type": "number",
@@ -12265,26 +7843,6 @@
"type": "number",
"description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
"fill": {
"anyOf": [
{
@@ -12315,47 +7873,6 @@
"type": "boolean",
"description": "Clip marks to the chart area."
},
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
"fillPattern": {
"type": "string",
"enum": [
@@ -12363,99 +7880,6 @@
"none"
],
"description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
}
},
"required": [
@@ -12498,7 +7922,7 @@
"subtract",
"grow"
],
- "description": "How chrome height interacts with the container height budget.\n\n- `'subtract'` (default): the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\n`'grow'` is a no-op for faceted specs in this version."
+ "description": "How chrome height interacts with the container height budget.\n\n- omitted: only notes and the series summary grow the figure; title, subtitle, and source subtract from the height budget.\n- `'subtract'`: the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\nFacets and layers report engine-owned growth once."
},
"title": {
"anyOf": [
@@ -12578,7 +8002,7 @@
"$ref": "#/definitions/EndpointLabelsConfig"
}
],
- "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show` and the legacy end-of-line labels."
+ "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show`."
},
"responsive": {
"anyOf": [
@@ -12595,10 +8019,6 @@
"$ref": "#/definitions/ThemeConfig",
"description": "Theme configuration overrides."
},
- "darkMode": {
- "$ref": "#/definitions/DarkMode",
- "deprecated": "Use the mount darkMode option; the spec field has no effect."
- },
"watermark": {
"type": "boolean",
"description": "Whether to show the OpenData watermark. Defaults to true; auto-hides when the container is under 200px tall (the cramped height class) unless explicitly set."
@@ -12682,101 +8102,20 @@
"mark": {
"anyOf": [
{
- "type": "string",
- "const": "bar"
- },
- {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
- "type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
+ "type": "string",
+ "const": "bar"
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "orient": {
+ "type": "string",
+ "enum": [
+ "horizontal",
+ "vertical"
],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
+ "description": "Explicit orientation override for bar marks."
},
"cornerRadius": {
"anyOf": [
@@ -12790,6 +8129,10 @@
],
"description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
},
+ "type": {
+ "type": "string",
+ "const": "bar"
+ },
"size": {
"type": "number",
"description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
@@ -12806,26 +8149,6 @@
"type": "number",
"description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
"fill": {
"anyOf": [
{
@@ -12856,47 +8179,6 @@
"type": "boolean",
"description": "Clip marks to the chart area."
},
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
"fillPattern": {
"type": "string",
"enum": [
@@ -12904,99 +8186,6 @@
"none"
],
"description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
}
},
"required": [
@@ -13039,7 +8228,7 @@
"subtract",
"grow"
],
- "description": "How chrome height interacts with the container height budget.\n\n- `'subtract'` (default): the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\n`'grow'` is a no-op for faceted specs in this version."
+ "description": "How chrome height interacts with the container height budget.\n\n- omitted: only notes and the series summary grow the figure; title, subtitle, and source subtract from the height budget.\n- `'subtract'`: the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\nFacets and layers report engine-owned growth once."
},
"title": {
"anyOf": [
@@ -13119,7 +8308,7 @@
"$ref": "#/definitions/EndpointLabelsConfig"
}
],
- "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show` and the legacy end-of-line labels."
+ "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show`."
},
"responsive": {
"anyOf": [
@@ -13136,10 +8325,6 @@
"$ref": "#/definitions/ThemeConfig",
"description": "Theme configuration overrides."
},
- "darkMode": {
- "$ref": "#/definitions/DarkMode",
- "deprecated": "Use the mount darkMode option; the spec field has no effect."
- },
"watermark": {
"type": "boolean",
"description": "Whether to show the OpenData watermark. Defaults to true; auto-hides when the container is under 200px tall (the cramped height class) unless explicitly set."
@@ -13230,10 +8415,6 @@
"type": "object",
"additionalProperties": false,
"properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
"point": {
"anyOf": [
{
@@ -13272,64 +8453,9 @@
],
"description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
},
- "orient": {
+ "type": {
"type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
- },
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
+ "const": "area"
},
"size": {
"type": "number",
@@ -13340,32 +8466,12 @@
"description": "Whether the mark is filled (vs stroked only)."
},
"opacity": {
- "type": "number",
- "description": "Default opacity (0-1)."
- },
- "fillOpacity": {
- "type": "number",
- "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
- },
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
+ "type": "number",
+ "description": "Default opacity (0-1)."
},
- "steps": {
+ "fillOpacity": {
"type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
+ "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
"fill": {
"anyOf": [
@@ -13397,47 +8503,6 @@
"type": "boolean",
"description": "Clip marks to the chart area."
},
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
"fillPattern": {
"type": "string",
"enum": [
@@ -13445,99 +8510,6 @@
"none"
],
"description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
}
},
"required": [
@@ -13580,7 +8552,7 @@
"subtract",
"grow"
],
- "description": "How chrome height interacts with the container height budget.\n\n- `'subtract'` (default): the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\n`'grow'` is a no-op for faceted specs in this version."
+ "description": "How chrome height interacts with the container height budget.\n\n- omitted: only notes and the series summary grow the figure; title, subtitle, and source subtract from the height budget.\n- `'subtract'`: the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\nFacets and layers report engine-owned growth once."
},
"title": {
"anyOf": [
@@ -13660,7 +8632,7 @@
"$ref": "#/definitions/EndpointLabelsConfig"
}
],
- "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show` and the legacy end-of-line labels."
+ "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show`."
},
"responsive": {
"anyOf": [
@@ -13677,10 +8649,6 @@
"$ref": "#/definitions/ThemeConfig",
"description": "Theme configuration overrides."
},
- "darkMode": {
- "$ref": "#/definitions/DarkMode",
- "deprecated": "Use the mount darkMode option; the spec field has no effect."
- },
"watermark": {
"type": "boolean",
"description": "Whether to show the OpenData watermark. Defaults to true; auto-hides when the container is under 200px tall (the cramped height class) unless explicitly set."
@@ -13771,106 +8739,20 @@
"type": "object",
"additionalProperties": false,
"properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
+ "trendline": {
"anyOf": [
{
"type": "boolean"
},
{
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
+ "$ref": "#/definitions/TrendlineConfig"
}
],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
+ "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
},
- "orient": {
+ "type": {
"type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
- },
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
+ "const": "point"
},
"size": {
"type": "number",
@@ -13884,29 +8766,9 @@
"type": "number",
"description": "Default opacity (0-1)."
},
- "fillOpacity": {
- "type": "number",
- "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
- },
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
+ "fillOpacity": {
"type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
+ "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
"fill": {
"anyOf": [
@@ -13938,47 +8800,6 @@
"type": "boolean",
"description": "Clip marks to the chart area."
},
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
"fillPattern": {
"type": "string",
"enum": [
@@ -13986,99 +8807,6 @@
"none"
],
"description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
}
},
"required": [
@@ -14121,7 +8849,7 @@
"subtract",
"grow"
],
- "description": "How chrome height interacts with the container height budget.\n\n- `'subtract'` (default): the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\n`'grow'` is a no-op for faceted specs in this version."
+ "description": "How chrome height interacts with the container height budget.\n\n- omitted: only notes and the series summary grow the figure; title, subtitle, and source subtract from the height budget.\n- `'subtract'`: the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\nFacets and layers report engine-owned growth once."
},
"title": {
"anyOf": [
@@ -14201,7 +8929,7 @@
"$ref": "#/definitions/EndpointLabelsConfig"
}
],
- "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show` and the legacy end-of-line labels."
+ "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show`."
},
"responsive": {
"anyOf": [
@@ -14218,10 +8946,6 @@
"$ref": "#/definitions/ThemeConfig",
"description": "Theme configuration overrides."
},
- "darkMode": {
- "$ref": "#/definitions/DarkMode",
- "deprecated": "Use the mount darkMode option; the spec field has no effect."
- },
"watermark": {
"type": "boolean",
"description": "Whether to show the OpenData watermark. Defaults to true; auto-hides when the container is under 200px tall (the cramped height class) unless explicitly set."
@@ -14313,105 +9037,8 @@
"additionalProperties": false,
"properties": {
"type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
"type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
- },
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
+ "const": "circle"
},
"size": {
"type": "number",
@@ -14425,29 +9052,9 @@
"type": "number",
"description": "Default opacity (0-1)."
},
- "fillOpacity": {
- "type": "number",
- "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
- },
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
+ "fillOpacity": {
"type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
+ "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
"fill": {
"anyOf": [
@@ -14479,47 +9086,6 @@
"type": "boolean",
"description": "Clip marks to the chart area."
},
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
"fillPattern": {
"type": "string",
"enum": [
@@ -14527,99 +9093,6 @@
"none"
],
"description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
}
},
"required": [
@@ -14662,7 +9135,7 @@
"subtract",
"grow"
],
- "description": "How chrome height interacts with the container height budget.\n\n- `'subtract'` (default): the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\n`'grow'` is a no-op for faceted specs in this version."
+ "description": "How chrome height interacts with the container height budget.\n\n- omitted: only notes and the series summary grow the figure; title, subtitle, and source subtract from the height budget.\n- `'subtract'`: the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\nFacets and layers report engine-owned growth once."
},
"title": {
"anyOf": [
@@ -14742,7 +9215,7 @@
"$ref": "#/definitions/EndpointLabelsConfig"
}
],
- "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show` and the legacy end-of-line labels."
+ "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show`."
},
"responsive": {
"anyOf": [
@@ -14759,10 +9232,6 @@
"$ref": "#/definitions/ThemeConfig",
"description": "Theme configuration overrides."
},
- "darkMode": {
- "$ref": "#/definitions/DarkMode",
- "deprecated": "Use the mount darkMode option; the spec field has no effect."
- },
"watermark": {
"type": "boolean",
"description": "Whether to show the OpenData watermark. Defaults to true; auto-hides when the container is under 200px tall (the cramped height class) unless explicitly set."
@@ -14854,105 +9323,8 @@
"additionalProperties": false,
"properties": {
"type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
"type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
- },
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
+ "const": "lollipop"
},
"size": {
"type": "number",
@@ -14964,31 +9336,11 @@
},
"opacity": {
"type": "number",
- "description": "Default opacity (0-1)."
- },
- "fillOpacity": {
- "type": "number",
- "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
- },
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
+ "description": "Default opacity (0-1)."
},
- "steps": {
+ "fillOpacity": {
"type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
+ "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
"fill": {
"anyOf": [
@@ -15020,47 +9372,6 @@
"type": "boolean",
"description": "Clip marks to the chart area."
},
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
"fillPattern": {
"type": "string",
"enum": [
@@ -15068,99 +9379,6 @@
"none"
],
"description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
}
},
"required": [
@@ -15203,7 +9421,7 @@
"subtract",
"grow"
],
- "description": "How chrome height interacts with the container height budget.\n\n- `'subtract'` (default): the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\n`'grow'` is a no-op for faceted specs in this version."
+ "description": "How chrome height interacts with the container height budget.\n\n- omitted: only notes and the series summary grow the figure; title, subtitle, and source subtract from the height budget.\n- `'subtract'`: the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\nFacets and layers report engine-owned growth once."
},
"title": {
"anyOf": [
@@ -15283,7 +9501,7 @@
"$ref": "#/definitions/EndpointLabelsConfig"
}
],
- "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show` and the legacy end-of-line labels."
+ "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show`."
},
"responsive": {
"anyOf": [
@@ -15300,10 +9518,6 @@
"$ref": "#/definitions/ThemeConfig",
"description": "Theme configuration overrides."
},
- "darkMode": {
- "$ref": "#/definitions/DarkMode",
- "deprecated": "Use the mount darkMode option; the spec field has no effect."
- },
"watermark": {
"type": "boolean",
"description": "Whether to show the OpenData watermark. Defaults to true; auto-hides when the container is under 200px tall (the cramped height class) unless explicitly set."
@@ -15376,124 +9590,27 @@
},
"required": [
"data",
- "encoding",
- "mark"
- ]
- },
- {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "mark": {
- "anyOf": [
- {
- "type": "string",
- "const": "beeswarm"
- },
- {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
- "type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
- },
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
+ "encoding",
+ "mark"
+ ]
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "mark": {
+ "anyOf": [
+ {
+ "type": "string",
+ "const": "beeswarm"
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "beeswarm"
},
"size": {
"type": "number",
@@ -15511,26 +9628,6 @@
"type": "number",
"description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
"fill": {
"anyOf": [
{
@@ -15561,47 +9658,6 @@
"type": "boolean",
"description": "Clip marks to the chart area."
},
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
"fillPattern": {
"type": "string",
"enum": [
@@ -15609,99 +9665,6 @@
"none"
],
"description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
}
},
"required": [
@@ -15744,7 +9707,7 @@
"subtract",
"grow"
],
- "description": "How chrome height interacts with the container height budget.\n\n- `'subtract'` (default): the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\n`'grow'` is a no-op for faceted specs in this version."
+ "description": "How chrome height interacts with the container height budget.\n\n- omitted: only notes and the series summary grow the figure; title, subtitle, and source subtract from the height budget.\n- `'subtract'`: the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\nFacets and layers report engine-owned growth once."
},
"title": {
"anyOf": [
@@ -15824,7 +9787,7 @@
"$ref": "#/definitions/EndpointLabelsConfig"
}
],
- "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show` and the legacy end-of-line labels."
+ "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show`."
},
"responsive": {
"anyOf": [
@@ -15841,10 +9804,6 @@
"$ref": "#/definitions/ThemeConfig",
"description": "Theme configuration overrides."
},
- "darkMode": {
- "$ref": "#/definitions/DarkMode",
- "deprecated": "Use the mount darkMode option; the spec field has no effect."
- },
"watermark": {
"type": "boolean",
"description": "Whether to show the OpenData watermark. Defaults to true; auto-hides when the container is under 200px tall (the cramped height class) unless explicitly set."
@@ -15896,145 +9855,61 @@
"$ref": "#/definitions/SeriesSearchConfig"
}
],
- "description": "Searchable series highlight (\"find your country\"). Renders a compact search input above the chart with typeahead over the color channel's distinct values. Selecting a value adds it to the highlight set (multi-select chips, removable); clearing restores the authored `encoding.color.highlight` baseline. Requires a categorical color encoding. Mutually exclusive with edit mode (search wins)."
- },
- "youDrawIt": {
- "$ref": "#/definitions/YouDrawItConfig",
- "description": "\"You draw it\" interactive engagement format: the reader draws their guess of the trend before the real line reveals. Line marks only, single-series only. Mutually exclusive with edit mode (validation warning if both are enabled; youDrawIt is disabled)."
- },
- "display": {
- "$ref": "#/definitions/Display",
- "description": "Display mode controlling how much chart chrome is rendered.\n\n- `'full'` (default): full publication chart with chrome, axes, legend, padding.\n- `'sparkline'`: inline mini-chart for dashboards/KPI cards. Strips chrome, axes, legend, watermark, animation, and crosshair. The mark fills the container edge-to-edge with a small safety margin. Best with `mark: 'line' | 'area' | 'bar' | 'point'`.\n\n**Override precedence:** explicit user fields always win, even in sparkline mode. Setting `chrome.title` on a sparkline still renders the title. Setting `legend: { show: true }` still renders the legend. This applies at top-level and per-breakpoint overrides."
- },
- "resolve": {
- "$ref": "#/definitions/ResolveConfig",
- "description": "Resolution strategy for shared/independent scales and axes across facet panels. Only meaningful when `encoding.facet` is present. Shared scales (the default) let readers compare across panels; independent scales give each panel its own domain."
- },
- "zIndex": {
- "type": "number",
- "description": "Render order within a LayerSpec. Higher values render on top. When omitted, layers render in array order (later layers paint on top). Axis assignment (left/right y) is always determined by array position, not zIndex."
- }
- },
- "required": [
- "data",
- "encoding",
- "mark"
- ]
- },
- {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "mark": {
- "anyOf": [
- {
- "type": "string",
- "const": "range"
- },
- {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
+ "description": "Searchable series highlight (\"find your country\"). Renders a compact search input above the chart with typeahead over the color channel's distinct values. Selecting a value adds it to the highlight set (multi-select chips, removable); clearing restores the authored `encoding.color.highlight` baseline. Requires a categorical color encoding. Mutually exclusive with edit mode (search wins)."
+ },
+ "youDrawIt": {
+ "$ref": "#/definitions/YouDrawItConfig",
+ "description": "\"You draw it\" interactive engagement format: the reader draws their guess of the trend before the real line reveals. Line marks only, single-series only. Mutually exclusive with edit mode (validation warning if both are enabled; youDrawIt is disabled)."
+ },
+ "display": {
+ "$ref": "#/definitions/Display",
+ "description": "Display mode controlling how much chart chrome is rendered.\n\n- `'full'` (default): full publication chart with chrome, axes, legend, padding.\n- `'sparkline'`: inline mini-chart for dashboards/KPI cards. Strips chrome, axes, legend, watermark, animation, and crosshair. The mark fills the container edge-to-edge with a small safety margin. Best with `mark: 'line' | 'area' | 'bar' | 'point'`.\n\n**Override precedence:** explicit user fields always win, even in sparkline mode. Setting `chrome.title` on a sparkline still renders the title. Setting `legend: { show: true }` still renders the legend. This applies at top-level and per-breakpoint overrides."
+ },
+ "resolve": {
+ "$ref": "#/definitions/ResolveConfig",
+ "description": "Resolution strategy for shared/independent scales and axes across facet panels. Only meaningful when `encoding.facet` is present. Shared scales (the default) let readers compare across panels; independent scales give each panel its own domain."
+ },
+ "zIndex": {
+ "type": "number",
+ "description": "Render order within a LayerSpec. Higher values render on top. When omitted, layers render in array order (later layers paint on top). Axis assignment (left/right y) is always determined by array position, not zIndex."
+ }
+ },
+ "required": [
+ "data",
+ "encoding",
+ "mark"
+ ]
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "mark": {
+ "anyOf": [
+ {
+ "type": "string",
+ "const": "range"
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "style": {
"type": "string",
"enum": [
- "horizontal",
- "vertical"
+ "dumbbell",
+ "arrow",
+ "bar"
],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
+ "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
},
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
+ "colorByDirection": {
+ "type": "boolean",
+ "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
},
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
+ "type": {
+ "type": "string",
+ "const": "range"
},
"size": {
"type": "number",
@@ -16052,26 +9927,6 @@
"type": "number",
"description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
"fill": {
"anyOf": [
{
@@ -16102,47 +9957,6 @@
"type": "boolean",
"description": "Clip marks to the chart area."
},
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
"fillPattern": {
"type": "string",
"enum": [
@@ -16150,99 +9964,6 @@
"none"
],
"description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
}
},
"required": [
@@ -16285,7 +10006,7 @@
"subtract",
"grow"
],
- "description": "How chrome height interacts with the container height budget.\n\n- `'subtract'` (default): the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\n`'grow'` is a no-op for faceted specs in this version."
+ "description": "How chrome height interacts with the container height budget.\n\n- omitted: only notes and the series summary grow the figure; title, subtitle, and source subtract from the height budget.\n- `'subtract'`: the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\nFacets and layers report engine-owned growth once."
},
"title": {
"anyOf": [
@@ -16365,7 +10086,7 @@
"$ref": "#/definitions/EndpointLabelsConfig"
}
],
- "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show` and the legacy end-of-line labels."
+ "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show`."
},
"responsive": {
"anyOf": [
@@ -16382,10 +10103,6 @@
"$ref": "#/definitions/ThemeConfig",
"description": "Theme configuration overrides."
},
- "darkMode": {
- "$ref": "#/definitions/DarkMode",
- "deprecated": "Use the mount darkMode option; the spec field has no effect."
- },
"watermark": {
"type": "boolean",
"description": "Whether to show the OpenData watermark. Defaults to true; auto-hides when the container is under 200px tall (the cramped height class) unless explicitly set."
@@ -16476,94 +10193,36 @@
"type": "object",
"additionalProperties": false,
"properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
- "type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
+ "units": {
"type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
+ "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
},
- "endAngle": {
+ "columns": {
"type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
+ "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
},
- "centerLabel": {
+ "other": {
"anyOf": [
{
- "type": "string"
+ "type": "number"
},
{
"type": "object",
"properties": {
- "text": {
- "type": "string"
+ "threshold": {
+ "type": "number"
},
- "subtitle": {
+ "label": {
"type": "string"
}
},
"required": [
- "text"
+ "threshold"
],
"additionalProperties": false
}
],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
+ "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
},
"cornerRadius": {
"anyOf": [
@@ -16577,6 +10236,10 @@
],
"description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
},
+ "type": {
+ "type": "string",
+ "const": "waffle"
+ },
"size": {
"type": "number",
"description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
@@ -16593,197 +10256,43 @@
"type": "number",
"description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
- "fill": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "$ref": "#/definitions/GradientDef"
- }
- ],
- "description": "Default fill color or gradient."
- },
- "stroke": {
- "type": "string",
- "description": "Default stroke color."
- },
- "strokeWidth": {
- "type": "number",
- "description": "Default stroke width."
- },
- "tooltip": {
- "type": [
- "boolean",
- "null"
- ],
- "description": "Tooltip behavior. null disables tooltips."
- },
- "clip": {
- "type": "boolean",
- "description": "Clip marks to the chart area."
- },
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
- "fillPattern": {
- "type": "string",
- "enum": [
- "auto",
- "none"
- ],
- "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
+ "fill": {
"anyOf": [
{
- "type": "boolean"
+ "type": "string"
},
{
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
+ "$ref": "#/definitions/GradientDef"
}
],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
+ "description": "Default fill color or gradient."
+ },
+ "stroke": {
+ "type": "string",
+ "description": "Default stroke color."
+ },
+ "strokeWidth": {
+ "type": "number",
+ "description": "Default stroke width."
+ },
+ "tooltip": {
+ "type": [
+ "boolean",
+ "null"
+ ],
+ "description": "Tooltip behavior. null disables tooltips."
+ },
+ "clip": {
+ "type": "boolean",
+ "description": "Clip marks to the chart area."
+ },
+ "fillPattern": {
+ "type": "string",
+ "enum": [
+ "auto",
+ "none"
+ ],
+ "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
}
},
"required": [
@@ -16826,7 +10335,7 @@
"subtract",
"grow"
],
- "description": "How chrome height interacts with the container height budget.\n\n- `'subtract'` (default): the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\n`'grow'` is a no-op for faceted specs in this version."
+ "description": "How chrome height interacts with the container height budget.\n\n- omitted: only notes and the series summary grow the figure; title, subtitle, and source subtract from the height budget.\n- `'subtract'`: the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\nFacets and layers report engine-owned growth once."
},
"title": {
"anyOf": [
@@ -16906,7 +10415,7 @@
"$ref": "#/definitions/EndpointLabelsConfig"
}
],
- "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show` and the legacy end-of-line labels."
+ "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show`."
},
"responsive": {
"anyOf": [
@@ -16923,10 +10432,6 @@
"$ref": "#/definitions/ThemeConfig",
"description": "Theme configuration overrides."
},
- "darkMode": {
- "$ref": "#/definitions/DarkMode",
- "deprecated": "Use the mount darkMode option; the spec field has no effect."
- },
"watermark": {
"type": "boolean",
"description": "Whether to show the OpenData watermark. Defaults to true; auto-hides when the container is under 200px tall (the cramped height class) unless explicitly set."
@@ -16981,220 +10486,42 @@
"description": "Searchable series highlight (\"find your country\"). Renders a compact search input above the chart with typeahead over the color channel's distinct values. Selecting a value adds it to the highlight set (multi-select chips, removable); clearing restores the authored `encoding.color.highlight` baseline. Requires a categorical color encoding. Mutually exclusive with edit mode (search wins)."
},
"youDrawIt": {
- "$ref": "#/definitions/YouDrawItConfig",
- "description": "\"You draw it\" interactive engagement format: the reader draws their guess of the trend before the real line reveals. Line marks only, single-series only. Mutually exclusive with edit mode (validation warning if both are enabled; youDrawIt is disabled)."
- },
- "display": {
- "$ref": "#/definitions/Display",
- "description": "Display mode controlling how much chart chrome is rendered.\n\n- `'full'` (default): full publication chart with chrome, axes, legend, padding.\n- `'sparkline'`: inline mini-chart for dashboards/KPI cards. Strips chrome, axes, legend, watermark, animation, and crosshair. The mark fills the container edge-to-edge with a small safety margin. Best with `mark: 'line' | 'area' | 'bar' | 'point'`.\n\n**Override precedence:** explicit user fields always win, even in sparkline mode. Setting `chrome.title` on a sparkline still renders the title. Setting `legend: { show: true }` still renders the legend. This applies at top-level and per-breakpoint overrides."
- },
- "resolve": {
- "$ref": "#/definitions/ResolveConfig",
- "description": "Resolution strategy for shared/independent scales and axes across facet panels. Only meaningful when `encoding.facet` is present. Shared scales (the default) let readers compare across panels; independent scales give each panel its own domain."
- },
- "zIndex": {
- "type": "number",
- "description": "Render order within a LayerSpec. Higher values render on top. When omitted, layers render in array order (later layers paint on top). Axis assignment (left/right y) is always determined by array position, not zIndex."
- }
- },
- "required": [
- "data",
- "encoding",
- "mark"
- ]
- },
- {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "mark": {
- "anyOf": [
- {
- "type": "string",
- "const": "text"
- },
- {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
- "type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
- },
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
- },
- "size": {
- "type": "number",
- "description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
- },
- "filled": {
- "type": "boolean",
- "description": "Whether the mark is filled (vs stroked only)."
- },
- "opacity": {
- "type": "number",
- "description": "Default opacity (0-1)."
- },
- "fillOpacity": {
- "type": "number",
- "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
- },
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
- "fill": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "$ref": "#/definitions/GradientDef"
- }
- ],
- "description": "Default fill color or gradient."
- },
- "stroke": {
- "type": "string",
- "description": "Default stroke color."
- },
- "strokeWidth": {
- "type": "number",
- "description": "Default stroke width."
- },
- "tooltip": {
- "type": [
- "boolean",
- "null"
- ],
- "description": "Tooltip behavior. null disables tooltips."
- },
- "clip": {
- "type": "boolean",
- "description": "Clip marks to the chart area."
- },
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
+ "$ref": "#/definitions/YouDrawItConfig",
+ "description": "\"You draw it\" interactive engagement format: the reader draws their guess of the trend before the real line reveals. Line marks only, single-series only. Mutually exclusive with edit mode (validation warning if both are enabled; youDrawIt is disabled)."
+ },
+ "display": {
+ "$ref": "#/definitions/Display",
+ "description": "Display mode controlling how much chart chrome is rendered.\n\n- `'full'` (default): full publication chart with chrome, axes, legend, padding.\n- `'sparkline'`: inline mini-chart for dashboards/KPI cards. Strips chrome, axes, legend, watermark, animation, and crosshair. The mark fills the container edge-to-edge with a small safety margin. Best with `mark: 'line' | 'area' | 'bar' | 'point'`.\n\n**Override precedence:** explicit user fields always win, even in sparkline mode. Setting `chrome.title` on a sparkline still renders the title. Setting `legend: { show: true }` still renders the legend. This applies at top-level and per-breakpoint overrides."
+ },
+ "resolve": {
+ "$ref": "#/definitions/ResolveConfig",
+ "description": "Resolution strategy for shared/independent scales and axes across facet panels. Only meaningful when `encoding.facet` is present. Shared scales (the default) let readers compare across panels; independent scales give each panel its own domain."
+ },
+ "zIndex": {
+ "type": "number",
+ "description": "Render order within a LayerSpec. Higher values render on top. When omitted, layers render in array order (later layers paint on top). Axis assignment (left/right y) is always determined by array position, not zIndex."
+ }
+ },
+ "required": [
+ "data",
+ "encoding",
+ "mark"
+ ]
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "mark": {
+ "anyOf": [
+ {
+ "type": "string",
+ "const": "text"
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
"dx": {
"type": "number",
"description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
@@ -17225,106 +10552,63 @@
"type": "number",
"description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
},
- "fillPattern": {
+ "type": {
"type": "string",
- "enum": [
- "auto",
- "none"
- ],
- "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
+ "const": "text"
},
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
+ "size": {
+ "type": "number",
+ "description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
},
- "colorByDirection": {
+ "filled": {
"type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
+ "description": "Whether the mark is filled (vs stroked only)."
},
- "units": {
+ "opacity": {
"type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
+ "description": "Default opacity (0-1)."
},
- "columns": {
+ "fillOpacity": {
"type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
+ "description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "other": {
+ "fill": {
"anyOf": [
{
- "type": "number"
+ "type": "string"
},
{
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
+ "$ref": "#/definitions/GradientDef"
}
],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
+ "description": "Default fill color or gradient."
},
- "weekStart": {
+ "stroke": {
"type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
+ "description": "Default stroke color."
},
- "cellRadius": {
+ "strokeWidth": {
"type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
+ "description": "Default stroke width."
},
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
+ "tooltip": {
+ "type": [
+ "boolean",
+ "null"
],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
+ "description": "Tooltip behavior. null disables tooltips."
},
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
+ "clip": {
+ "type": "boolean",
+ "description": "Clip marks to the chart area."
+ },
+ "fillPattern": {
+ "type": "string",
+ "enum": [
+ "auto",
+ "none"
],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
+ "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
}
},
"required": [
@@ -17367,7 +10651,7 @@
"subtract",
"grow"
],
- "description": "How chrome height interacts with the container height budget.\n\n- `'subtract'` (default): the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\n`'grow'` is a no-op for faceted specs in this version."
+ "description": "How chrome height interacts with the container height budget.\n\n- omitted: only notes and the series summary grow the figure; title, subtitle, and source subtract from the height budget.\n- `'subtract'`: the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\nFacets and layers report engine-owned growth once."
},
"title": {
"anyOf": [
@@ -17447,7 +10731,7 @@
"$ref": "#/definitions/EndpointLabelsConfig"
}
],
- "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show` and the legacy end-of-line labels."
+ "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show`."
},
"responsive": {
"anyOf": [
@@ -17464,10 +10748,6 @@
"$ref": "#/definitions/ThemeConfig",
"description": "Theme configuration overrides."
},
- "darkMode": {
- "$ref": "#/definitions/DarkMode",
- "deprecated": "Use the mount darkMode option; the spec field has no effect."
- },
"watermark": {
"type": "boolean",
"description": "Whether to show the OpenData watermark. Defaults to true; auto-hides when the container is under 200px tall (the cramped height class) unless explicitly set."
@@ -17548,116 +10828,19 @@
"type": "object",
"additionalProperties": false,
"properties": {
- "mark": {
- "anyOf": [
- {
- "type": "string",
- "const": "tick"
- },
- {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
- "type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
- },
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
+ "mark": {
+ "anyOf": [
+ {
+ "type": "string",
+ "const": "tick"
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "tick"
},
"size": {
"type": "number",
@@ -17675,26 +10858,6 @@
"type": "number",
"description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
"fill": {
"anyOf": [
{
@@ -17725,47 +10888,6 @@
"type": "boolean",
"description": "Clip marks to the chart area."
},
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
"fillPattern": {
"type": "string",
"enum": [
@@ -17773,99 +10895,6 @@
"none"
],
"description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
}
},
"required": [
@@ -17908,7 +10937,7 @@
"subtract",
"grow"
],
- "description": "How chrome height interacts with the container height budget.\n\n- `'subtract'` (default): the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\n`'grow'` is a no-op for faceted specs in this version."
+ "description": "How chrome height interacts with the container height budget.\n\n- omitted: only notes and the series summary grow the figure; title, subtitle, and source subtract from the height budget.\n- `'subtract'`: the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\nFacets and layers report engine-owned growth once."
},
"title": {
"anyOf": [
@@ -17988,7 +11017,7 @@
"$ref": "#/definitions/EndpointLabelsConfig"
}
],
- "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show` and the legacy end-of-line labels."
+ "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show`."
},
"responsive": {
"anyOf": [
@@ -18005,10 +11034,6 @@
"$ref": "#/definitions/ThemeConfig",
"description": "Theme configuration overrides."
},
- "darkMode": {
- "$ref": "#/definitions/DarkMode",
- "deprecated": "Use the mount darkMode option; the spec field has no effect."
- },
"watermark": {
"type": "boolean",
"description": "Whether to show the OpenData watermark. Defaults to true; auto-hides when the container is under 200px tall (the cramped height class) unless explicitly set."
@@ -18099,95 +11124,6 @@
"type": "object",
"additionalProperties": false,
"properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
- "type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
- },
"cornerRadius": {
"anyOf": [
{
@@ -18200,6 +11136,10 @@
],
"description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
},
+ "type": {
+ "type": "string",
+ "const": "rect"
+ },
"size": {
"type": "number",
"description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
@@ -18216,26 +11156,6 @@
"type": "number",
"description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
"fill": {
"anyOf": [
{
@@ -18266,47 +11186,6 @@
"type": "boolean",
"description": "Clip marks to the chart area."
},
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
"fillPattern": {
"type": "string",
"enum": [
@@ -18314,99 +11193,6 @@
"none"
],
"description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
}
},
"required": [
@@ -18449,7 +11235,7 @@
"subtract",
"grow"
],
- "description": "How chrome height interacts with the container height budget.\n\n- `'subtract'` (default): the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\n`'grow'` is a no-op for faceted specs in this version."
+ "description": "How chrome height interacts with the container height budget.\n\n- omitted: only notes and the series summary grow the figure; title, subtitle, and source subtract from the height budget.\n- `'subtract'`: the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\nFacets and layers report engine-owned growth once."
},
"title": {
"anyOf": [
@@ -18529,7 +11315,7 @@
"$ref": "#/definitions/EndpointLabelsConfig"
}
],
- "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show` and the legacy end-of-line labels."
+ "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show`."
},
"responsive": {
"anyOf": [
@@ -18546,10 +11332,6 @@
"$ref": "#/definitions/ThemeConfig",
"description": "Theme configuration overrides."
},
- "darkMode": {
- "$ref": "#/definitions/DarkMode",
- "deprecated": "Use the mount darkMode option; the spec field has no effect."
- },
"watermark": {
"type": "boolean",
"description": "Whether to show the OpenData watermark. Defaults to true; auto-hides when the container is under 200px tall (the cramped height class) unless explicitly set."
@@ -18640,106 +11422,9 @@
"type": "object",
"additionalProperties": false,
"properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
- "type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
- },
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
+ "type": {
+ "type": "string",
+ "const": "rule"
},
"size": {
"type": "number",
@@ -18757,26 +11442,6 @@
"type": "number",
"description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
"fill": {
"anyOf": [
{
@@ -18807,47 +11472,6 @@
"type": "boolean",
"description": "Clip marks to the chart area."
},
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
"fillPattern": {
"type": "string",
"enum": [
@@ -18855,99 +11479,6 @@
"none"
],
"description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
}
},
"required": [
@@ -18990,7 +11521,7 @@
"subtract",
"grow"
],
- "description": "How chrome height interacts with the container height budget.\n\n- `'subtract'` (default): the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\n`'grow'` is a no-op for faceted specs in this version."
+ "description": "How chrome height interacts with the container height budget.\n\n- omitted: only notes and the series summary grow the figure; title, subtitle, and source subtract from the height budget.\n- `'subtract'`: the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\nFacets and layers report engine-owned growth once."
},
"title": {
"anyOf": [
@@ -19070,7 +11601,7 @@
"$ref": "#/definitions/EndpointLabelsConfig"
}
],
- "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show` and the legacy end-of-line labels."
+ "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show`."
},
"responsive": {
"anyOf": [
@@ -19087,10 +11618,6 @@
"$ref": "#/definitions/ThemeConfig",
"description": "Theme configuration overrides."
},
- "darkMode": {
- "$ref": "#/definitions/DarkMode",
- "deprecated": "Use the mount darkMode option; the spec field has no effect."
- },
"watermark": {
"type": "boolean",
"description": "Whether to show the OpenData watermark. Defaults to true; auto-hides when the container is under 200px tall (the cramped height class) unless explicitly set."
@@ -19179,108 +11706,23 @@
},
{
"type": "object",
- "additionalProperties": false,
- "properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
- "type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
- },
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
+ "additionalProperties": false,
+ "properties": {
+ "weekStart": {
+ "type": "string",
+ "enum": [
+ "monday",
+ "sunday"
],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
+ "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
+ },
+ "cellRadius": {
+ "type": "number",
+ "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
+ },
+ "type": {
+ "type": "string",
+ "const": "calendar"
},
"size": {
"type": "number",
@@ -19298,26 +11740,6 @@
"type": "number",
"description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
"fill": {
"anyOf": [
{
@@ -19348,47 +11770,6 @@
"type": "boolean",
"description": "Clip marks to the chart area."
},
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
"fillPattern": {
"type": "string",
"enum": [
@@ -19396,99 +11777,6 @@
"none"
],
"description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
}
},
"required": [
@@ -19531,7 +11819,7 @@
"subtract",
"grow"
],
- "description": "How chrome height interacts with the container height budget.\n\n- `'subtract'` (default): the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\n`'grow'` is a no-op for faceted specs in this version."
+ "description": "How chrome height interacts with the container height budget.\n\n- omitted: only notes and the series summary grow the figure; title, subtitle, and source subtract from the height budget.\n- `'subtract'`: the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\nFacets and layers report engine-owned growth once."
},
"title": {
"anyOf": [
@@ -19611,7 +11899,7 @@
"$ref": "#/definitions/EndpointLabelsConfig"
}
],
- "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show` and the legacy end-of-line labels."
+ "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show`."
},
"responsive": {
"anyOf": [
@@ -19628,10 +11916,6 @@
"$ref": "#/definitions/ThemeConfig",
"description": "Theme configuration overrides."
},
- "darkMode": {
- "$ref": "#/definitions/DarkMode",
- "deprecated": "Use the mount darkMode option; the spec field has no effect."
- },
"watermark": {
"type": "boolean",
"description": "Whether to show the OpenData watermark. Defaults to true; auto-hides when the container is under 200px tall (the cramped height class) unless explicitly set."
@@ -19722,106 +12006,46 @@
"type": "object",
"additionalProperties": false,
"properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
- "orient": {
+ "shape": {
"type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
+ "const": "hemicycle",
+ "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
},
- "centerLabel": {
+ "seatRadius": {
"anyOf": [
{
- "type": "string"
+ "type": "number"
},
{
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
+ "type": "string",
+ "const": "auto"
}
],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
+ "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
},
- "cornerRadius": {
+ "majorityLine": {
"anyOf": [
{
- "type": "number"
+ "type": "boolean"
},
{
- "type": "string",
- "const": "pill"
+ "type": "object",
+ "properties": {
+ "seats": {
+ "type": "number"
+ },
+ "label": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
}
],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
+ "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
+ },
+ "type": {
+ "type": "string",
+ "const": "parliament"
},
"size": {
"type": "number",
@@ -19839,26 +12063,6 @@
"type": "number",
"description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
"fill": {
"anyOf": [
{
@@ -19889,47 +12093,6 @@
"type": "boolean",
"description": "Clip marks to the chart area."
},
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
"fillPattern": {
"type": "string",
"enum": [
@@ -19937,99 +12100,6 @@
"none"
],
"description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
- ],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
}
},
"required": [
@@ -20072,7 +12142,7 @@
"subtract",
"grow"
],
- "description": "How chrome height interacts with the container height budget.\n\n- `'subtract'` (default): the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\n`'grow'` is a no-op for faceted specs in this version."
+ "description": "How chrome height interacts with the container height budget.\n\n- omitted: only notes and the series summary grow the figure; title, subtitle, and source subtract from the height budget.\n- `'subtract'`: the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\nFacets and layers report engine-owned growth once."
},
"title": {
"anyOf": [
@@ -20152,7 +12222,7 @@
"$ref": "#/definitions/EndpointLabelsConfig"
}
],
- "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show` and the legacy end-of-line labels."
+ "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show`."
},
"responsive": {
"anyOf": [
@@ -20169,10 +12239,6 @@
"$ref": "#/definitions/ThemeConfig",
"description": "Theme configuration overrides."
},
- "darkMode": {
- "$ref": "#/definitions/DarkMode",
- "deprecated": "Use the mount darkMode option; the spec field has no effect."
- },
"watermark": {
"type": "boolean",
"description": "Whether to show the OpenData watermark. Defaults to true; auto-hides when the container is under 200px tall (the cramped height class) unless explicitly set."
@@ -20226,85 +12292,43 @@
],
"description": "Searchable series highlight (\"find your country\"). Renders a compact search input above the chart with typeahead over the color channel's distinct values. Selecting a value adds it to the highlight set (multi-select chips, removable); clearing restores the authored `encoding.color.highlight` baseline. Requires a categorical color encoding. Mutually exclusive with edit mode (search wins)."
},
- "youDrawIt": {
- "$ref": "#/definitions/YouDrawItConfig",
- "description": "\"You draw it\" interactive engagement format: the reader draws their guess of the trend before the real line reveals. Line marks only, single-series only. Mutually exclusive with edit mode (validation warning if both are enabled; youDrawIt is disabled)."
- },
- "display": {
- "$ref": "#/definitions/Display",
- "description": "Display mode controlling how much chart chrome is rendered.\n\n- `'full'` (default): full publication chart with chrome, axes, legend, padding.\n- `'sparkline'`: inline mini-chart for dashboards/KPI cards. Strips chrome, axes, legend, watermark, animation, and crosshair. The mark fills the container edge-to-edge with a small safety margin. Best with `mark: 'line' | 'area' | 'bar' | 'point'`.\n\n**Override precedence:** explicit user fields always win, even in sparkline mode. Setting `chrome.title` on a sparkline still renders the title. Setting `legend: { show: true }` still renders the legend. This applies at top-level and per-breakpoint overrides."
- },
- "resolve": {
- "$ref": "#/definitions/ResolveConfig",
- "description": "Resolution strategy for shared/independent scales and axes across facet panels. Only meaningful when `encoding.facet` is present. Shared scales (the default) let readers compare across panels; independent scales give each panel its own domain."
- },
- "zIndex": {
- "type": "number",
- "description": "Render order within a LayerSpec. Higher values render on top. When omitted, layers render in array order (later layers paint on top). Axis assignment (left/right y) is always determined by array position, not zIndex."
- }
- },
- "required": [
- "data",
- "encoding",
- "mark"
- ]
- },
- {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "mark": {
- "anyOf": [
- {
- "type": "string",
- "const": "histogram"
- },
- {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
- "point": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "const": "transparent"
- },
- {
- "type": "string",
- "const": "endpoints"
- },
- {
- "type": "string",
- "const": "last"
- },
- {
- "type": "string",
- "const": "first"
- }
- ],
- "description": "Show point markers on line/area marks.\n- true: filled circles at each data point\n- 'transparent': invisible hover targets (legacy behavior)\n- 'endpoints': show only first and last point per series (hollow dots)\n- 'last' / 'first': show only the last (or first) point — filled dot, no white halo. Useful for highlighting the latest reading on a line chart and the default for sparkline mode.\n- false: no point marks (default; uses voronoi overlay for tooltips)"
- },
- "interpolate": {
- "type": "string",
- "enum": [
- "linear",
- "monotone",
- "step",
- "step-before",
- "step-after",
- "basis",
- "cardinal",
- "natural"
- ],
- "description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
- },
+ "youDrawIt": {
+ "$ref": "#/definitions/YouDrawItConfig",
+ "description": "\"You draw it\" interactive engagement format: the reader draws their guess of the trend before the real line reveals. Line marks only, single-series only. Mutually exclusive with edit mode (validation warning if both are enabled; youDrawIt is disabled)."
+ },
+ "display": {
+ "$ref": "#/definitions/Display",
+ "description": "Display mode controlling how much chart chrome is rendered.\n\n- `'full'` (default): full publication chart with chrome, axes, legend, padding.\n- `'sparkline'`: inline mini-chart for dashboards/KPI cards. Strips chrome, axes, legend, watermark, animation, and crosshair. The mark fills the container edge-to-edge with a small safety margin. Best with `mark: 'line' | 'area' | 'bar' | 'point'`.\n\n**Override precedence:** explicit user fields always win, even in sparkline mode. Setting `chrome.title` on a sparkline still renders the title. Setting `legend: { show: true }` still renders the legend. This applies at top-level and per-breakpoint overrides."
+ },
+ "resolve": {
+ "$ref": "#/definitions/ResolveConfig",
+ "description": "Resolution strategy for shared/independent scales and axes across facet panels. Only meaningful when `encoding.facet` is present. Shared scales (the default) let readers compare across panels; independent scales give each panel its own domain."
+ },
+ "zIndex": {
+ "type": "number",
+ "description": "Render order within a LayerSpec. Higher values render on top. When omitted, layers render in array order (later layers paint on top). Axis assignment (left/right y) is always determined by array position, not zIndex."
+ }
+ },
+ "required": [
+ "data",
+ "encoding",
+ "mark"
+ ]
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "mark": {
+ "anyOf": [
+ {
+ "type": "string",
+ "const": "histogram"
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
"orient": {
"type": "string",
"enum": [
@@ -20313,45 +12337,6 @@
],
"description": "Explicit orientation override for bar marks."
},
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
- "type": "number",
- "description": "Outer radius for arc marks."
- },
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
- },
- "endAngle": {
- "type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
- },
"cornerRadius": {
"anyOf": [
{
@@ -20364,6 +12349,18 @@
],
"description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
},
+ "binCount": {
+ "type": "number",
+ "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
+ },
+ "normalize": {
+ "type": "boolean",
+ "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
+ },
+ "type": {
+ "type": "string",
+ "const": "histogram"
+ },
"size": {
"type": "number",
"description": "Bar/column marks: fixed bar thickness in pixels. When set, bars are this height (horizontal) or width (vertical), centered within the band.\n\nPoint marks: the dot RADIUS in pixels (default 5, typical 2-12). This is NOT Vega-Lite's point `size`, which is an area in px^2 -- a VL size of 110 corresponds to `size: 6` here (r = sqrt(area / pi)). Validation rejects values above 50 with the conversion."
@@ -20380,26 +12377,6 @@
"type": "number",
"description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
"fill": {
"anyOf": [
{
@@ -20421,156 +12398,22 @@
},
"tooltip": {
"type": [
- "boolean",
- "null"
- ],
- "description": "Tooltip behavior. null disables tooltips."
- },
- "clip": {
- "type": "boolean",
- "description": "Clip marks to the chart area."
- },
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
- "fillPattern": {
- "type": "string",
- "enum": [
- "auto",
- "none"
- ],
- "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
+ "boolean",
+ "null"
],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
+ "description": "Tooltip behavior. null disables tooltips."
+ },
+ "clip": {
+ "type": "boolean",
+ "description": "Clip marks to the chart area."
+ },
+ "fillPattern": {
+ "type": "string",
+ "enum": [
+ "auto",
+ "none"
+ ],
+ "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
}
},
"required": [
@@ -20613,7 +12456,7 @@
"subtract",
"grow"
],
- "description": "How chrome height interacts with the container height budget.\n\n- `'subtract'` (default): the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\n`'grow'` is a no-op for faceted specs in this version."
+ "description": "How chrome height interacts with the container height budget.\n\n- omitted: only notes and the series summary grow the figure; title, subtitle, and source subtract from the height budget.\n- `'subtract'`: the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\nFacets and layers report engine-owned growth once."
},
"title": {
"anyOf": [
@@ -20693,7 +12536,7 @@
"$ref": "#/definitions/EndpointLabelsConfig"
}
],
- "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show` and the legacy end-of-line labels."
+ "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show`."
},
"responsive": {
"anyOf": [
@@ -20710,10 +12553,6 @@
"$ref": "#/definitions/ThemeConfig",
"description": "Theme configuration overrides."
},
- "darkMode": {
- "$ref": "#/definitions/DarkMode",
- "deprecated": "Use the mount darkMode option; the spec field has no effect."
- },
"watermark": {
"type": "boolean",
"description": "Whether to show the OpenData watermark. Defaults to true; auto-hides when the container is under 200px tall (the cramped height class) unless explicitly set."
@@ -20804,10 +12643,6 @@
"type": "object",
"additionalProperties": false,
"properties": {
- "type": {
- "$ref": "#/definitions/MarkType",
- "description": "The mark type."
- },
"point": {
"anyOf": [
{
@@ -20846,64 +12681,21 @@
],
"description": "Curve interpolation for line/area marks. Maps to d3-shape curve factories."
},
- "orient": {
- "type": "string",
- "enum": [
- "horizontal",
- "vertical"
- ],
- "description": "Explicit orientation override for bar marks."
- },
- "innerRadius": {
- "type": "number",
- "description": "Inner radius for arc marks. >0 produces a donut, 0 or omitted produces a pie."
- },
- "outerRadius": {
+ "bandwidth": {
"type": "number",
- "description": "Outer radius for arc marks."
+ "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
},
- "startAngle": {
- "type": "number",
- "description": "Start angle in radians for arc marks. Defaults to 0 (full pie/donut). Combined with `endAngle`, restricts the pie/donut to a partial sweep, e.g. `startAngle: -Math.PI / 2, endAngle: Math.PI / 2` for a half-donut (election-style results donut). Angles follow d3's convention: 0 is straight up, increasing clockwise. Only meaningful when `type` is `'arc'`."
+ "cumulative": {
+ "type": "boolean",
+ "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
},
- "endAngle": {
+ "steps": {
"type": "number",
- "description": "End angle in radians for arc marks. Defaults to 2*Math.PI (full pie/donut). See `startAngle`. Only meaningful when `type` is `'arc'`."
- },
- "centerLabel": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "subtitle": {
- "type": "string"
- }
- },
- "required": [
- "text"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Donut center stat. A string renders one line; the object form adds a caption under it. Decorative (the number is already in the slices), and only meaningful when `type` is `'arc'` with an inner radius."
+ "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
},
- "cornerRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "pill"
- }
- ],
- "description": "Corner radius for rect/bar marks. 'pill' sets rx to half the bar thickness."
+ "type": {
+ "type": "string",
+ "const": "density"
},
"size": {
"type": "number",
@@ -20921,26 +12713,6 @@
"type": "number",
"description": "Fill opacity (0-1), independent of stroke (Vega-Lite aligned).\n\nOn a binned bar with more than one color group this defaults to 0.55 so overlapping distributions stay readable; set it explicitly to override. On an area, setting it switches the fill from the default gradient to a flat translucent series color — gradients and overlapping fills don't mix."
},
- "binCount": {
- "type": "number",
- "description": "Number of bins. Only meaningful when `type` is `'histogram'`; desugars to `x.bin.maxbins`. Defaults to 20. For a fixed bin width or a pinned extent, write `x.bin` directly — it is the canonical form and takes precedence over this shorthand."
- },
- "normalize": {
- "type": "boolean",
- "description": "Plot each group's share of its own total instead of a raw count. Only meaningful when `type` is `'histogram'`. Makes two distributions of very different size directly comparable."
- },
- "bandwidth": {
- "type": "number",
- "description": "Gaussian kernel bandwidth. Only meaningful when `type` is `'density'`. Omitted uses Silverman's rule of thumb."
- },
- "cumulative": {
- "type": "boolean",
- "description": "Draw the cumulative distribution instead of the density. Only meaningful when `type` is `'density'`."
- },
- "steps": {
- "type": "number",
- "description": "Number of points the density curve is evaluated at. Only meaningful when `type` is `'density'`. Defaults to 200."
- },
"fill": {
"anyOf": [
{
@@ -20956,162 +12728,28 @@
"type": "string",
"description": "Default stroke color."
},
- "strokeWidth": {
- "type": "number",
- "description": "Default stroke width."
- },
- "tooltip": {
- "type": [
- "boolean",
- "null"
- ],
- "description": "Tooltip behavior. null disables tooltips."
- },
- "clip": {
- "type": "boolean",
- "description": "Clip marks to the chart area."
- },
- "trendline": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "$ref": "#/definitions/TrendlineConfig"
- }
- ],
- "description": "Overlay a least-squares regression line on scatter (point) marks. Defaults to true. Set `mark: { type: 'point', trendline: false }` to suppress it when the chart already carries its own reference line (e.g. a manual x=y diagonal in a separate layer), which would otherwise produce two competing diagonals.\n\nPass an object to control stacking order: the line renders `below` the points by default, but a dense scatter (thousands of overlapping dots) fully occludes a line drawn underneath, so `{ layer: 'above' }` lifts the reference line over the cloud."
- },
- "dx": {
- "type": "number",
- "description": "Horizontal pixel offset from the data anchor. Only meaningful when `type` is `'text'`. Use it to lift a label clear of the mark it annotates instead of shifting the underlying data value."
- },
- "dy": {
- "type": "number",
- "description": "Vertical pixel offset from the data anchor (negative = up). Only meaningful when `type` is `'text'`. `{ type: 'text', dy: -10 }` floats each label ten pixels above its point."
- },
- "align": {
- "type": "string",
- "enum": [
- "left",
- "center",
- "right"
- ],
- "description": "Horizontal alignment of text marks relative to their anchor. Defaults to `'center'`."
- },
- "baseline": {
- "type": "string",
- "enum": [
- "top",
- "middle",
- "bottom"
- ],
- "description": "Vertical alignment of text marks relative to their anchor. Defaults to `'middle'`."
- },
- "fontSize": {
- "type": "number",
- "description": "Font size in pixels for text marks. Ignored when the `size` encoding channel is present (which scales font size from a data field). `size` is already taken by bar thickness, hence the distinct name."
- },
- "fillPattern": {
- "type": "string",
- "enum": [
- "auto",
- "none"
- ],
- "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
- },
- "style": {
- "type": "string",
- "enum": [
- "dumbbell",
- "arrow",
- "bar"
- ],
- "description": "Visual style for range marks. Only meaningful when `type` is `'range'`.\n- 'dumbbell' (default): a dot at each end joined by a connector line. The start dot is muted, the end dot carries the accent color.\n- 'arrow': a line with an arrowhead at the x2/y2 end, the strongest \"change between two points in time\" form.\n- 'bar': a plain floating range bar spanning start to end."
- },
- "colorByDirection": {
- "type": "boolean",
- "description": "Color range marks by direction of change. Only meaningful when `type` is `'range'`. When true, increases (x2 > x, or y2 > y) use the theme's `positive` semantic color and decreases use `negative`. A field-based `encoding.color` takes precedence over direction coloring."
- },
- "units": {
- "type": "number",
- "description": "Total number of cells in a waffle grid. Only meaningful when `type` is `'waffle'`. Category values normalize to this many cells via largest-remainder rounding so the grid always sums exactly. Default 100."
- },
- "columns": {
- "type": "number",
- "description": "Number of columns in a waffle grid. Only meaningful when `type` is `'waffle'`. Rows derive from `units / columns`. Default 10."
- },
- "other": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "object",
- "properties": {
- "threshold": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "required": [
- "threshold"
- ],
- "additionalProperties": false
- }
- ],
- "description": "Opt-in \"Other\" bucketing for waffle grids. Only meaningful when `type` is `'waffle'`. Categories whose share of the total falls below `threshold` (a fraction, so `0.05` is 5%) merge into one trailing category. Off by default: bucketing is an editorial decision, not a layout default. Same shape as `SankeySpec.other`."
- },
- "weekStart": {
- "type": "string",
- "enum": [
- "monday",
- "sunday"
- ],
- "description": "First day of the week for calendar marks. Only meaningful when `type` is `'calendar'`. Controls which weekday occupies the top row of each band. Defaults to 'monday' (ISO week convention)."
- },
- "cellRadius": {
- "type": "number",
- "description": "Corner radius in pixels for calendar day cells. Only meaningful when `type` is `'calendar'`. Defaults to 2."
- },
- "shape": {
- "type": "string",
- "const": "hemicycle",
- "description": "Seat layout shape for parliament marks. Only meaningful when `type` is `'parliament'`. `'hemicycle'` (default, and currently the only shape) packs seats into concentric semicircular arcs, party-grouped left to right."
- },
- "seatRadius": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "const": "auto"
- }
- ],
- "description": "Seat dot radius in pixels for parliament marks. `'auto'` (default) sizes dots to fill the available hemicycle rings for the given seat count. Only meaningful when `type` is `'parliament'`."
- },
- "majorityLine": {
- "anyOf": [
- {
- "type": "boolean"
- },
- {
- "type": "object",
- "properties": {
- "seats": {
- "type": "number"
- },
- "label": {
- "type": "string"
- }
- },
- "additionalProperties": false
- }
+ "strokeWidth": {
+ "type": "number",
+ "description": "Default stroke width."
+ },
+ "tooltip": {
+ "type": [
+ "boolean",
+ "null"
],
- "description": "The majority-threshold line and \"N to win\" label on parliament marks. Only meaningful when `type` is `'parliament'`. `true` (default) draws the line at a simple majority (half the seats plus one); `false` hides it. The object form overrides the threshold and label: `{ seats }` sets a custom seat count (e.g. a two-thirds supermajority) and `{ label }` overrides the default \"N to win\" text. A `seats` value outside `1..totalSeats` warns and falls back to the simple-majority default."
+ "description": "Tooltip behavior. null disables tooltips."
+ },
+ "clip": {
+ "type": "boolean",
+ "description": "Clip marks to the chart area."
+ },
+ "fillPattern": {
+ "type": "string",
+ "enum": [
+ "auto",
+ "none"
+ ],
+ "description": "Pattern-fill reinforcement for filled marks (bar, area, arc).\n\n- `'auto'`: assign a per-series SVG pattern (diagonal hatch, dots, crosshatch, vertical lines) layered over each series color so series remain distinguishable without color vision. Marks too small to show a legible pattern keep their solid fill.\n- `'none'` (default): solid fills only."
}
},
"required": [
@@ -21154,7 +12792,7 @@
"subtract",
"grow"
],
- "description": "How chrome height interacts with the container height budget.\n\n- `'subtract'` (default): the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\n`'grow'` is a no-op for faceted specs in this version."
+ "description": "How chrome height interacts with the container height budget.\n\n- omitted: only notes and the series summary grow the figure; title, subtitle, and source subtract from the height budget.\n- `'subtract'`: the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\nFacets and layers report engine-owned growth once."
},
"title": {
"anyOf": [
@@ -21234,7 +12872,7 @@
"$ref": "#/definitions/EndpointLabelsConfig"
}
],
- "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show` and the legacy end-of-line labels."
+ "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show`."
},
"responsive": {
"anyOf": [
@@ -21251,10 +12889,6 @@
"$ref": "#/definitions/ThemeConfig",
"description": "Theme configuration overrides."
},
- "darkMode": {
- "$ref": "#/definitions/DarkMode",
- "deprecated": "Use the mount darkMode option; the spec field has no effect."
- },
"watermark": {
"type": "boolean",
"description": "Whether to show the OpenData watermark. Defaults to true; auto-hides when the container is under 200px tall (the cramped height class) unless explicitly set."
@@ -21428,17 +13062,20 @@
"description": "Legend display configuration."
},
"responsive": {
- "type": "boolean",
- "description": "Whether the chart adapts to container width. Defaults to true."
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "$ref": "#/definitions/ResponsiveConfig"
+ }
+ ],
+ "description": "Responsive behavior for the composed chart, with the same meaning as `ChartSpec.responsive`: `true`/`false` enables/disables container-width adaptation, and a `ResponsiveConfig` object gives fine-grained control (e.g. `{ compactAnnotationLimit: null }`). When omitted, the first leaf layer's `responsive` applies."
},
"theme": {
"$ref": "#/definitions/ThemeConfig",
"description": "Theme configuration overrides."
},
- "darkMode": {
- "$ref": "#/definitions/DarkMode",
- "deprecated": "Use the mount darkMode option; the spec field has no effect."
- },
"watermark": {
"type": "boolean",
"description": "Whether to show the OpenData watermark. Defaults to true."
@@ -21526,10 +13163,6 @@
"$ref": "#/definitions/ThemeConfig",
"description": "Theme configuration overrides."
},
- "darkMode": {
- "$ref": "#/definitions/DarkMode",
- "deprecated": "Use the mount darkMode option; the spec field has no effect."
- },
"watermark": {
"type": "boolean",
"description": "Whether to show the OpenData watermark. Defaults to true."
@@ -21591,10 +13224,6 @@
],
"description": "Sticky footer summing every quantitative column over the filtered rows (the whole result set, not just the current page). `true` uses the label \"Total\"."
},
- "compact": {
- "type": "boolean",
- "deprecated": "Use `density: 'condensed'`. Still honored, with a warning."
- },
"responsive": {
"type": "boolean",
"description": "Whether the table adapts to container width. Defaults to true."
@@ -21951,10 +13580,6 @@
"$ref": "#/definitions/ThemeConfig",
"description": "Theme configuration overrides."
},
- "darkMode": {
- "$ref": "#/definitions/DarkMode",
- "deprecated": "Use the mount darkMode option; the spec field has no effect."
- },
"animation": {
"$ref": "#/definitions/GraphAnimationSpec",
"description": "Motion configuration. DEFAULT-ON: omit for full defaults, `false` to opt out of choreography (layout warmup still applies)."
@@ -22491,10 +14116,6 @@
"$ref": "#/definitions/ThemeConfig",
"description": "Theme configuration overrides."
},
- "darkMode": {
- "$ref": "#/definitions/DarkMode",
- "deprecated": "Use the mount darkMode option; the spec field has no effect."
- },
"watermark": {
"type": "boolean",
"description": "Whether to show the OpenData watermark. Defaults to true."
@@ -22502,11 +14123,6 @@
"animation": {
"$ref": "#/definitions/AnimationSpec",
"description": "Animation configuration for entrance animations."
- },
- "valueFormat": {
- "type": "string",
- "description": "d3-format string applied to flow values in tooltips and ARIA labels. Uses the literal suffix extension: \".0f%\" appends \"%\" to the formatted number (data value 28 renders as \"28%\"). For currency: \"$,.0f\" or \"$~s\". For SI suffixes: \"~s\" (renders 1000 as \"1k\"). When not set, values use the default number formatter.",
- "deprecated": "Use the encoding channel's `format` field instead. This\nproperty is expanded into the encoding during compilation."
}
},
"required": [
@@ -22788,10 +14404,6 @@
"$ref": "#/definitions/ThemeConfig",
"description": "Theme configuration overrides."
},
- "darkMode": {
- "$ref": "#/definitions/DarkMode",
- "deprecated": "Use the mount darkMode option; the spec field has no effect."
- },
"watermark": {
"type": "boolean",
"description": "Whether to show the OpenData watermark. Defaults to true."
@@ -22893,6 +14505,19 @@
"$ref": "#/definitions/Chrome",
"description": "Editorial chrome (title, subtitle, source, byline, footer)."
},
+ "colorKey": {
+ "type": "object",
+ "properties": {
+ "activeValue": {
+ "type": "boolean"
+ },
+ "distribution": {
+ "type": "boolean"
+ }
+ },
+ "additionalProperties": false,
+ "description": "Optional static distribution and reader active-value presentation. Both default to false."
+ },
"legend": {
"$ref": "#/definitions/LegendConfig",
"description": "Legend display configuration."
@@ -22901,10 +14526,6 @@
"$ref": "#/definitions/ThemeConfig",
"description": "Theme configuration overrides."
},
- "darkMode": {
- "$ref": "#/definitions/DarkMode",
- "deprecated": "Use the mount darkMode option; the spec field has no effect."
- },
"watermark": {
"type": "boolean",
"description": "Whether to show the OpenData watermark. Defaults to true."
@@ -22913,14 +14534,16 @@
"$ref": "#/definitions/GeoMapPointsLayer",
"description": "Optional point/symbol layer rendered above choropleth features."
},
+ "callouts": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/GeoCallout"
+ },
+ "description": "Authored labels with explicit feature, point, or coordinate anchors."
+ },
"animation": {
"$ref": "#/definitions/AnimationSpec",
"description": "Animation configuration for entrance animations."
- },
- "valueFormat": {
- "type": "string",
- "description": "d3-format string applied to color values in tooltips and legend.",
- "deprecated": "Use the encoding channel's `format` field instead."
}
},
"required": [
@@ -22934,6 +14557,30 @@
"GeoMapGeo": {
"type": "object",
"properties": {
+ "objectKey": {
+ "type": "string",
+ "description": "Primary topology object; omission retains first-object behavior."
+ },
+ "fitObjectKey": {
+ "type": "string",
+ "description": "Fit against this primary-topology object, independent of contextual detail."
+ },
+ "sourcePlane": {
+ "type": "string",
+ "description": "Named assertion of the decoded coordinate plane for identity detail."
+ },
+ "detailLayers": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/GeoDetailLayer"
+ }
+ },
+ "insets": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/GeoInset"
+ }
+ },
"features": {
"description": "TopoJSON topology object. User imports this from us-atlas, world-atlas, or their own source."
},
@@ -22974,6 +14621,91 @@
"additionalProperties": false,
"description": "Geo configuration for map specs."
},
+ "GeoDetailLayer": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "features": {},
+ "objectKey": {
+ "type": "string"
+ },
+ "minZoom": {
+ "type": "number"
+ },
+ "maxZoom": {
+ "type": "number"
+ },
+ "sourcePlane": {
+ "type": "string",
+ "description": "Assertion that decoded identity coordinates share the primary source plane."
+ }
+ },
+ "required": [
+ "id",
+ "features",
+ "minZoom"
+ ],
+ "additionalProperties": false,
+ "description": "Context geometry uses the primary view's immutable fitted projection."
+ },
+ "GeoInset": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "label": {
+ "type": "string"
+ },
+ "featureIds": {
+ "type": "array",
+ "items": {
+ "type": [
+ "string",
+ "number"
+ ]
+ },
+ "description": "Canonical topology feature IDs, independent of the geo.idField data-join field."
+ },
+ "placement": {
+ "type": "string",
+ "enum": [
+ "bottom-left",
+ "bottom-right"
+ ]
+ },
+ "width": {
+ "type": "number"
+ },
+ "projection": {
+ "$ref": "#/definitions/GeoMapProjection"
+ },
+ "removeFromMain": {
+ "type": "boolean",
+ "description": "Defaults true; false explicitly duplicates primary feature coverage."
+ },
+ "pointIds": {
+ "type": "array",
+ "items": {
+ "type": [
+ "string",
+ "number"
+ ]
+ },
+ "description": "Explicit existing points.key values; omission includes no points."
+ }
+ },
+ "required": [
+ "id",
+ "label",
+ "featureIds",
+ "placement",
+ "width"
+ ],
+ "additionalProperties": false
+ },
"GeoMapProjection": {
"type": "string",
"enum": [
@@ -23103,6 +14835,14 @@
"$ref": "#/definitions/EncodingChannel",
"description": "Color encoding (quantitative or nominal). Drives fill color of features. Optional when a points layer is present (basemap-only mode)."
},
+ "text": {
+ "$ref": "#/definitions/EncodingChannel",
+ "description": "Explicit feature label field, read from its joined row or same-named topology property."
+ },
+ "textPriority": {
+ "$ref": "#/definitions/EncodingChannel",
+ "description": "Higher finite quantitative values win label placement priority. Requires text."
+ },
"tooltip": {
"anyOf": [
{
@@ -23141,12 +14881,20 @@
},
"size": {
"$ref": "#/definitions/EncodingChannel",
- "description": "Optional quantitative size encoding. Area-proportional: r proportional to sqrt(value). No size legend is rendered in v1; explain the scale in chrome.subtitle if needed."
+ "description": "Optional quantitative size encoding. Area-proportional: r proportional to sqrt(value). A proportional size key explains the resolved radius scale."
},
"color": {
"$ref": "#/definitions/EncodingChannel",
"description": "Optional color encoding (nominal or quantitative). Independent scale from choropleth."
},
+ "text": {
+ "$ref": "#/definitions/EncodingChannel",
+ "description": "Explicit point label field. Requires a unique points.key."
+ },
+ "textPriority": {
+ "$ref": "#/definitions/EncodingChannel",
+ "description": "Quantitative label priority; defaults to the point size value. Requires text."
+ },
"tooltip": {
"anyOf": [
{
@@ -23178,6 +14926,150 @@
"additionalProperties": false,
"description": "Point/symbol layer rendered above choropleth features."
},
+ "GeoCallout": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "anchor": {
+ "$ref": "#/definitions/GeoAnchor"
+ },
+ "text": {
+ "type": "string"
+ },
+ "subtitle": {
+ "type": "string"
+ },
+ "priority": {
+ "type": "number"
+ },
+ "minZoom": {
+ "type": "number"
+ },
+ "maxZoom": {
+ "type": "number"
+ },
+ "offset": {
+ "type": "object",
+ "properties": {
+ "dx": {
+ "type": "number"
+ },
+ "dy": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "dx",
+ "dy"
+ ],
+ "additionalProperties": false
+ },
+ "maxWidth": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "id",
+ "anchor",
+ "text"
+ ],
+ "additionalProperties": false,
+ "description": "Static authored geographic explanation; plain text, without chart editing semantics."
+ },
+ "GeoAnchor": {
+ "anyOf": [
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "viewId": {
+ "type": "string"
+ },
+ "layerId": {
+ "type": "string"
+ },
+ "kind": {
+ "type": "string",
+ "const": "feature"
+ },
+ "id": {
+ "type": [
+ "string",
+ "number"
+ ]
+ }
+ },
+ "required": [
+ "id",
+ "kind"
+ ]
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "viewId": {
+ "type": "string"
+ },
+ "layerId": {
+ "type": "string"
+ },
+ "kind": {
+ "type": "string",
+ "const": "point"
+ },
+ "id": {
+ "type": [
+ "string",
+ "number"
+ ]
+ }
+ },
+ "required": [
+ "id",
+ "kind"
+ ]
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "viewId": {
+ "type": "string"
+ },
+ "layerId": {
+ "type": "string"
+ },
+ "kind": {
+ "type": "string",
+ "const": "coordinate"
+ },
+ "space": {
+ "type": "string",
+ "enum": [
+ "geographic",
+ "source"
+ ]
+ },
+ "x": {
+ "type": "number"
+ },
+ "y": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "kind",
+ "space",
+ "x",
+ "y"
+ ]
+ }
+ ],
+ "description": "Explicit map anchor; feature and point identities occupy separate namespaces."
+ },
"BarListSpec": {
"type": "object",
"properties": {
@@ -23225,10 +15117,6 @@
"$ref": "#/definitions/ThemeConfig",
"description": "Theme configuration overrides."
},
- "darkMode": {
- "$ref": "#/definitions/DarkMode",
- "deprecated": "Use the mount darkMode option; the spec field has no effect."
- },
"watermark": {
"type": "boolean",
"description": "Whether to show the OpenData watermark. Defaults to true."
@@ -23236,11 +15124,6 @@
"animation": {
"$ref": "#/definitions/AnimationSpec",
"description": "Animation configuration for entrance animations."
- },
- "valueFormat": {
- "type": "string",
- "description": "d3-format string applied to bar values and tooltips. Examples: \".1f\" for one decimal, \"$,.0f\" for currency, \"~s\" for SI.",
- "deprecated": "Use the encoding channel's `format` field instead. This\nproperty is expanded into the encoding during compilation."
}
},
"required": [
@@ -23332,7 +15215,7 @@
"subtract",
"grow"
],
- "description": "How chrome height interacts with the container height budget.\n\n- `'subtract'` (default): the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\n`'grow'` is a no-op for faceted specs in this version."
+ "description": "How chrome height interacts with the container height budget.\n\n- omitted: only notes and the series summary grow the figure; title, subtitle, and source subtract from the height budget.\n- `'subtract'`: the container height is fixed and chrome text (title/subtitle/source) is subtracted from it, shrinking the plot. This is the historical behavior.\n- `'grow'`: the container height is treated as the *plot* budget, and the rendered SVG grows taller by the measured chrome height. The plot keeps its full budget (minus axis/padding overhead) regardless of how many lines the title wraps to.\n\nFacets and layers report engine-owned growth once."
},
"title": {
"anyOf": [
@@ -23412,7 +15295,7 @@
"$ref": "#/definitions/EndpointLabelsConfig"
}
],
- "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show` and the legacy end-of-line labels."
+ "description": "Right-side endpoint labels column for multi-series line/area charts.\n\n- `true` or `EndpointLabelsConfig`: render the column.\n- `false`: hide the column.\n- omitted: auto-enable for multi-series line/area charts, hide otherwise.\n\nSee {@link EndpointLabelsConfig } for the full suppression truth table that relates this flag to `legend.show`."
},
"responsive": {
"anyOf": [
@@ -23429,10 +15312,6 @@
"$ref": "#/definitions/ThemeConfig",
"description": "Theme configuration overrides."
},
- "darkMode": {
- "$ref": "#/definitions/DarkMode",
- "deprecated": "Use the mount darkMode option; the spec field has no effect."
- },
"watermark": {
"type": "boolean",
"description": "Whether to show the OpenData watermark. Defaults to true; auto-hides when the container is under 200px tall (the cramped height class) unless explicitly set."
diff --git a/packages/core/src/accessibility/__tests__/alt-text.test.ts b/packages/core/src/accessibility/__tests__/alt-text.test.ts
index 068c8a0d..80948442 100644
--- a/packages/core/src/accessibility/__tests__/alt-text.test.ts
+++ b/packages/core/src/accessibility/__tests__/alt-text.test.ts
@@ -67,7 +67,7 @@ describe('generateAltText', () => {
it('handles missing chrome gracefully', () => {
const alt = generateAltText(barSpec, barSpec.data);
- expect(alt).toBeTruthy();
+ expect(alt).toContain('Bar chart');
expect(alt).not.toContain('showing undefined');
});
});
diff --git a/packages/core/src/accessibility/alt-text.ts b/packages/core/src/accessibility/alt-text.ts
index fa6b2e11..c4ce419a 100644
--- a/packages/core/src/accessibility/alt-text.ts
+++ b/packages/core/src/accessibility/alt-text.ts
@@ -27,7 +27,7 @@ export function generateAltText(spec: ChartSpec, data: DataRow[]): string {
let chartName = MARK_DISPLAY_NAMES[markType] ?? `${markType} chart`;
// Special case: donut detection
- if (markType === 'arc' && markDef.innerRadius && markDef.innerRadius > 0) {
+ if (markDef.type === 'arc' && markDef.innerRadius && markDef.innerRadius > 0) {
chartName = 'Donut chart';
}
@@ -56,7 +56,7 @@ export function generateAltText(spec: ChartSpec, data: DataRow[]): string {
// Waffle: name the unit framing. Per-category "x of N units" phrasing
// lives on the mark aria labels; the chart-level text carries the total.
- if (markType === 'waffle') {
+ if (markDef.type === 'waffle') {
const units = markDef.units ?? 100;
parts.push(`dividing ${units} units`);
}
diff --git a/packages/core/src/helpers/__tests__/edit-transactions.test.ts b/packages/core/src/helpers/__tests__/edit-transactions.test.ts
index fc0e420f..45913920 100644
--- a/packages/core/src/helpers/__tests__/edit-transactions.test.ts
+++ b/packages/core/src/helpers/__tests__/edit-transactions.test.ts
@@ -111,6 +111,23 @@ describe('applyEditTransaction', () => {
expect(spec).toEqual(before);
});
+ it('rejects reordered ID-less reflines by label', () => {
+ const spec = {
+ annotations: [
+ { type: 'refline', y: 1, label: 'Max' },
+ { type: 'refline', y: 2, label: 'Target' },
+ ],
+ };
+ const op = (index: number) => ({
+ op: 'set' as const,
+ path: `/annotations/${index}/labelOffset`,
+ value: { dx: 4 },
+ expect: { type: 'refline', text: 'Target' },
+ });
+ expect(applyEditTransaction(spec, { ops: [op(0)] }).ok).toBe(false);
+ expect(applyEditTransaction(spec, { ops: [op(1)] }).ok).toBe(true);
+ });
+
it.each([
{ op: 'set', path: '/missing/child', value: 1 },
{ op: 'remove', path: '/annotations/0' },
diff --git a/packages/core/src/helpers/__tests__/json-pointer.test.ts b/packages/core/src/helpers/__tests__/json-pointer.test.ts
new file mode 100644
index 00000000..01307b45
--- /dev/null
+++ b/packages/core/src/helpers/__tests__/json-pointer.test.ts
@@ -0,0 +1,12 @@
+import { describe, expect, it } from 'vitest';
+import { encodePointerToken, jsonPointer } from '../json-pointer';
+
+describe('jsonPointer', () => {
+ it('escapes tokens per RFC 6901 and treats no tokens as the root', () => {
+ expect(encodePointerToken('a/b~c')).toBe('a~1b~0c');
+ expect(encodePointerToken('~1')).toBe('~01');
+ expect(jsonPointer('data', 0, 'a/b')).toBe('/data/0/a~1b');
+ expect(jsonPointer('')).toBe('/');
+ expect(jsonPointer()).toBe('');
+ });
+});
diff --git a/packages/core/src/helpers/__tests__/spec-builders.test.ts b/packages/core/src/helpers/__tests__/spec-builders.test.ts
index dc396687..c009db60 100644
--- a/packages/core/src/helpers/__tests__/spec-builders.test.ts
+++ b/packages/core/src/helpers/__tests__/spec-builders.test.ts
@@ -58,9 +58,9 @@ describe('inferFieldType', () => {
expect(inferFieldType(data, 'period')).toBe('temporal');
});
- it('infers temporal for year-only strings (YYYY format)', () => {
+ it('infers quantitative for numeric year strings, matching compilation', () => {
const data = [{ year: '2020' }, { year: '2021' }];
- expect(inferFieldType(data, 'year')).toBe('temporal');
+ expect(inferFieldType(data, 'year')).toBe('quantitative');
});
it('infers nominal for plain string fields', () => {
@@ -83,10 +83,39 @@ describe('inferFieldType', () => {
expect(inferFieldType(data, 'date')).toBe('temporal');
});
- it('samples at most 20 values', () => {
- // Create 100 items, all numbers. Should still work without issue.
- const data = Array.from({ length: 100 }, (_, i) => ({ v: i }));
- expect(inferFieldType(data, 'v')).toBe('quantitative');
+ it.each([
+ { values: ['12', '15.5', '-3'], type: 'quantitative' },
+ { values: ['2020', '2021'], type: 'quantitative' },
+ { values: ['2020-01-01', '2021-01-01'], type: 'temporal' },
+ { values: [1, 2, 3, 4, 'other'], type: 'nominal' },
+ { values: [1, 2, 3, 4, 5, 'other'], type: 'quantitative' },
+ { values: ['2020-01', '2020-02'], type: 'temporal' },
+ { values: ['2020-01-01T12:30:00Z', '2020-01-02 08:00'], type: 'temporal' },
+ { values: ['2020-01-01T12:30:00.000+02:00'], type: 'temporal' },
+ { values: ['Jan 2020', 'February 2020', 'Mar 3, 2020'], type: 'temporal' },
+ { values: ['01/15/2020', '2020/01/16'], type: 'temporal' },
+ // Engine-dependent Date parsing accepts these; they are categories.
+ { values: ['Product 1', 'Product 2', 'Product 3'], type: 'nominal' },
+ { values: ['Region 2', 'North 10'], type: 'nominal' },
+ { values: ['item-1', 'panel 1', 'A: 10', '#000000', '10%'], type: 'nominal' },
+ { values: ['2020-13-45'], type: 'nominal' },
+ ])('uses compilation inference rules for $values', ({ values, type }) => {
+ expect(
+ inferFieldType(
+ values.map((value) => ({ value })),
+ 'value',
+ ),
+ ).toBe(type);
+ });
+
+ it('samples 50 rows, including evidence beyond the former 20-row window', () => {
+ const data = Array.from({ length: 100 }, (_, i) => ({ value: i < 20 ? i : 'other' }));
+ expect(inferFieldType(data, 'value')).toBe('nominal');
+ });
+
+ it('does not let rows after the first 50 change the inferred type', () => {
+ const data = Array.from({ length: 100 }, (_, i) => ({ value: i < 50 ? i : 'other' }));
+ expect(inferFieldType(data, 'value')).toBe('quantitative');
});
it('returns nominal for empty data', () => {
@@ -147,11 +176,10 @@ describe('lineChart', () => {
it('passes through theme without emitting ineffective spec darkMode', () => {
const spec = lineChart(timeSeriesData, 'date', 'value', {
theme: { borderRadius: 8 },
- darkMode: 'auto',
});
expect(spec.theme).toEqual({ borderRadius: 8 });
- expect(spec.darkMode).toBeUndefined();
+ expect(spec).not.toHaveProperty('darkMode');
});
});
@@ -290,8 +318,7 @@ describe('dataTable', () => {
search: true,
pagination: { pageSize: 10 },
stickyFirstColumn: true,
- compact: true,
- darkMode: 'auto',
+ density: 'condensed',
});
expect(spec.rowKey).toBe('name');
@@ -300,8 +327,8 @@ describe('dataTable', () => {
expect(spec.pagination).toEqual({ pageSize: 10 });
expect(spec.stickyFirstColumn).toBe(true);
expect(spec.density).toBe('condensed');
- expect(spec.compact).toBeUndefined();
- expect(spec.darkMode).toBeUndefined();
+ expect(spec).not.toHaveProperty('compact');
+ expect(spec).not.toHaveProperty('darkMode');
});
it('returns empty columns for empty data', () => {
diff --git a/packages/core/src/helpers/edit-transactions.ts b/packages/core/src/helpers/edit-transactions.ts
index 2437da83..5e8dc234 100644
--- a/packages/core/src/helpers/edit-transactions.ts
+++ b/packages/core/src/helpers/edit-transactions.ts
@@ -1,4 +1,5 @@
import type { EditOp, EditTransaction } from '../types/events';
+import { jsonPointer } from './json-pointer';
export type EditTransactionResult =
| { ok: true; spec: T; inverse: EditTransaction }
@@ -38,10 +39,6 @@ function tokens(path: string): string[] {
});
}
-function pointer(parts: string[]): string {
- return `/${parts.map((part) => part.replace(/~/g, '~0').replace(/\//g, '~1')).join('/')}`;
-}
-
function keyAt(parent: Container, token: string, insert = false): string | number {
if (!Array.isArray(parent)) return token;
if (insert && token === '-') return parent.length;
@@ -78,7 +75,9 @@ function checkExpectation(root: unknown, parts: string[], expected: EditOp['expe
if (!isContainer(target) || Array.isArray(target))
throw new Error('Annotation target is invalid.');
for (const field of ['id', 'type', 'text'] as const) {
- if (Object.hasOwn(expected, field) && target[field] !== expected[field]) {
+ // Reflines and ranges expose their text as `label`.
+ const actual = field === 'text' && target.type !== 'text' ? target.label : target[field];
+ if (Object.hasOwn(expected, field) && actual !== expected[field]) {
throw new Error(`Annotation ${field} changed; discard the stale edit.`);
}
}
@@ -108,7 +107,7 @@ export function applyEditTransaction(spec: T, tx: EditTransaction): EditTrans
if (op.op !== 'add' || Array.isArray(parent))
throw new Error('Intermediate container is missing.');
parent[key] = /^(0|[1-9]\d*|-)$/.test(parts[i + 1]) ? [] : {};
- created.push(pointer(parts.slice(0, i + 1)));
+ created.push(jsonPointer(...parts.slice(0, i + 1)));
}
parent = read(parent, key);
}
@@ -116,7 +115,7 @@ export function applyEditTransaction(spec: T, tx: EditTransaction): EditTrans
const key = keyAt(parent, parts.at(-1)!, op.op === 'add');
const exists = Object.hasOwn(parent, key);
const old = read(parent, key);
- const path = pointer([...parts.slice(0, -1), String(key)]);
+ const path = jsonPointer(...parts.slice(0, -1), key);
let undo: EditOp;
if (op.op === 'remove') {
if (!exists) throw new Error('Edit target no longer exists.');
diff --git a/packages/core/src/helpers/json-pointer.ts b/packages/core/src/helpers/json-pointer.ts
new file mode 100644
index 00000000..dc575355
--- /dev/null
+++ b/packages/core/src/helpers/json-pointer.ts
@@ -0,0 +1,9 @@
+/** Escape one RFC 6901 reference token: `~` becomes `~0`, then `/` becomes `~1`. */
+export function encodePointerToken(token: string | number): string {
+ return String(token).replace(/~/g, '~0').replace(/\//g, '~1');
+}
+
+/** Build a root-relative JSON Pointer from unescaped tokens. No tokens is the root, `''`. */
+export function jsonPointer(...tokens: ReadonlyArray): string {
+ return tokens.map((token) => `/${encodePointerToken(token)}`).join('');
+}
diff --git a/packages/core/src/helpers/spec-builders.ts b/packages/core/src/helpers/spec-builders.ts
index 187b53fb..9a738df5 100644
--- a/packages/core/src/helpers/spec-builders.ts
+++ b/packages/core/src/helpers/spec-builders.ts
@@ -14,7 +14,6 @@ import type {
Annotation,
ChartSpec,
Chrome,
- DarkMode,
DataRow,
Encoding,
EncodingChannel,
@@ -38,20 +37,6 @@ import type { ColumnConfig } from '../types/table';
/** A field reference: either a plain string (field name) or a full EncodingChannel. */
export type FieldRef = string | EncodingChannel;
-const warnedBuilderOptions = new Set();
-function warnBuilderOption(key: string, message: string): void {
- if (warnedBuilderOptions.has(key)) return;
- warnedBuilderOptions.add(key);
- console.warn(`[openchart] ${message}`);
-}
-function warnDarkMode(options?: { darkMode?: DarkMode }): void {
- if (options?.darkMode !== undefined)
- warnBuilderOption(
- 'darkMode',
- 'Builder darkMode is deprecated and has no effect; use the mount option.',
- );
-}
-
/** Common options shared by all chart builder functions. */
export interface ChartBuilderOptions {
/** Color encoding: field name or full channel config for series differentiation. */
@@ -66,8 +51,6 @@ export interface ChartBuilderOptions {
responsive?: boolean;
/** Theme configuration overrides. */
theme?: ThemeConfig;
- /** @deprecated Set darkMode on the mount instead; this option has no effect. */
- darkMode?: DarkMode;
}
/** Options for the dataTable builder. */
@@ -80,8 +63,6 @@ export interface TableBuilderOptions {
chrome?: Chrome;
/** Theme configuration overrides. */
theme?: ThemeConfig;
- /** @deprecated Set darkMode on the mount instead; this option has no effect. */
- darkMode?: DarkMode;
/** Enable client-side search/filter. */
search?: boolean;
/** Pagination configuration. */
@@ -90,8 +71,6 @@ export interface TableBuilderOptions {
stickyFirstColumn?: boolean;
/** Row density. */
density?: TableDensity;
- /** @deprecated Use density. */
- compact?: boolean;
/** Whether the table adapts to container width. */
responsive?: boolean;
}
@@ -100,49 +79,71 @@ export interface TableBuilderOptions {
// Type inference
// ---------------------------------------------------------------------------
-/** ISO 8601 date patterns we recognize for temporal inference. */
-const ISO_DATE_RE = /^\d{4}(-\d{2}(-\d{2}(T\d{2}(:\d{2}(:\d{2})?)?)?)?)?$/;
+/**
+ * Date-shaped strings: ISO 8601 dates and date-times, month-name dates
+ * ("Jan 2020", "March 3, 2020"), and slash dates. `Date` parsing alone is
+ * engine-dependent and accepts labels such as "Product 1".
+ */
+const DATE_STRING_RES = [
+ /^\d{4}-\d{2}(-\d{2}([T ]\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?)?)?$/,
+ /^(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*\.?( \d{1,2},?)? \d{4}$/i,
+ /^(\d{1,2}\/\d{1,2}\/\d{4}|\d{4}\/\d{1,2}\/\d{1,2})$/,
+];
/**
* Infer the encoding field type from data values.
*
- * Samples up to 20 values from the data array for the given field.
- * - If all sampled values are numbers (or null/undefined), returns "quantitative".
- * - If all sampled string values match ISO date patterns, returns "temporal".
- * - Otherwise returns "nominal".
+ * Samples 50 rows, ignoring nulls; more than 80% numeric or temporal values
+ * establish a type. Numeric strings (including ISO years) are quantitative.
+ * Strings count as temporal only when date-shaped and parseable.
*/
export function inferFieldType(data: DataRow[], field: string): FieldType {
- const sampleSize = Math.min(data.length, 20);
- let hasNumber = false;
- let hasDateString = false;
- let hasOtherString = false;
+ // Sample up to 50 rows for more reliable inference on mixed/messy data
+ const sampleSize = Math.min(50, data.length);
+ let numericCount = 0;
+ let dateCount = 0;
+ let totalNonNull = 0;
for (let i = 0; i < sampleSize; i++) {
const value = data[i][field];
-
- // Skip null/undefined
if (value == null) continue;
+ totalNonNull++;
- if (typeof value === 'number') {
- hasNumber = true;
- } else if (typeof value === 'string') {
- if (ISO_DATE_RE.test(value) && !Number.isNaN(Date.parse(value))) {
- hasDateString = true;
- } else {
- hasOtherString = true;
+ // Check numeric
+ if (typeof value === 'number' && Number.isFinite(value)) {
+ numericCount++;
+ continue;
+ }
+
+ // Check date-like strings
+ if (typeof value === 'string') {
+ // Try as number first
+ const num = Number(value);
+ if (!Number.isNaN(num) && Number.isFinite(num) && value.trim() !== '') {
+ numericCount++;
+ continue;
+ }
+
+ // Try as date
+ const date = new Date(value);
+ if (DATE_STRING_RES.some((re) => re.test(value)) && !Number.isNaN(date.getTime())) {
+ dateCount++;
+ continue;
}
- } else if (value instanceof Date) {
- hasDateString = true;
- } else {
- hasOtherString = true;
+ }
+
+ if (value instanceof Date && !Number.isNaN(value.getTime())) {
+ dateCount++;
}
}
- // Numbers-only: quantitative
- if (hasNumber && !hasDateString && !hasOtherString) return 'quantitative';
- // Date strings/Date objects only: temporal
- if (hasDateString && !hasNumber && !hasOtherString) return 'temporal';
- // Everything else: nominal
+ if (totalNonNull === 0) return 'nominal';
+
+ // If >80% of sampled values are dates, it's temporal
+ if (dateCount / totalNonNull > 0.8) return 'temporal';
+ // If >80% are numeric, it's quantitative
+ if (numericCount / totalNonNull > 0.8) return 'quantitative';
+ // Otherwise it's nominal
return 'nominal';
}
@@ -184,7 +185,6 @@ function buildChartSpec(
encoding: Encoding,
options?: ChartBuilderOptions,
): ChartSpec {
- warnDarkMode(options);
// Cast needed because the generic discriminated union can't be constructed
// from a runtime MarkType variable — the caller always pairs mark+encoding correctly.
const spec = { mark, data, encoding } as ChartSpec;
@@ -399,9 +399,6 @@ export function scatterChart(
* @param options - Column definitions, chrome, pagination, search, etc.
*/
export function dataTable(data: DataRow[], options?: TableBuilderOptions): TableSpec {
- warnDarkMode(options);
- if (options?.compact !== undefined)
- warnBuilderOption('compact', 'dataTable compact is deprecated; use density.');
// Auto-generate columns from data keys if not provided
let columns = options?.columns;
if (!columns && data.length > 0) {
@@ -431,7 +428,6 @@ export function dataTable(data: DataRow[], options?: TableBuilderOptions): Table
if (options?.pagination !== undefined) spec.pagination = options.pagination;
if (options?.stickyFirstColumn !== undefined) spec.stickyFirstColumn = options.stickyFirstColumn;
if (options?.density !== undefined) spec.density = options.density;
- else if (options?.compact !== undefined) spec.density = options.compact ? 'condensed' : 'regular';
if (options?.responsive !== undefined) spec.responsive = options.responsive;
return spec;
@@ -455,8 +451,6 @@ export interface TileMapBuilderOptions {
legend?: LegendConfig;
/** Theme overrides. */
theme?: ThemeConfig;
- /** @deprecated Set darkMode on the mount instead; this option has no effect. */
- darkMode?: DarkMode;
/** Whether to show the watermark. */
watermark?: boolean;
/** Animation configuration. */
@@ -482,7 +476,6 @@ export function tileMap(
data: Record | DataRow[],
options?: TileMapBuilderOptions,
): TileMapSpec {
- warnDarkMode(options);
return {
type: 'tilemap' as const,
data,
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index 66032c68..0a9c2ca9 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -13,6 +13,7 @@
export type { EditTransactionResult } from './helpers/edit-transactions';
export { applyEditTransaction } from './helpers/edit-transactions';
+export { encodePointerToken, jsonPointer } from './helpers/json-pointer';
export * from './types/index';
// ---------------------------------------------------------------------------
@@ -61,7 +62,6 @@ export {
broadsheet,
clarity,
DEFAULT_THEME,
- editorial,
essay,
legacyEditorial,
mergeThemeConfig,
@@ -83,7 +83,6 @@ export {
computeNoteBandHeight,
estimateTextWidth,
FOOTNOTE_LINE_HEIGHT,
- footnoteBandHeight,
heuristicMeasure,
resolveMeasurer,
textAscent,
@@ -128,7 +127,6 @@ export {
HPAD_COMPACT_MIN,
LABEL_GAP_COMPACT,
LABEL_GAP_DEFAULT,
- LABEL_GAP_NARROW_MAX,
MAX_LEFT_LABEL_FRACTION_COMPACT,
MAX_LEFT_LABEL_FRACTION_DEFAULT,
MAX_LEFT_LABEL_FRACTION_MEDIUM,
@@ -139,7 +137,6 @@ export {
TICK_LABEL_OFFSET,
TICK_LINE_HEIGHT_FACTOR,
TOP_PAD_EXTRA_NARROW,
- TOP_PAD_NARROW_MAX,
truncateRotatedLabel,
truncateToWidth,
X_AXIS_BAND_HEIGHT,
diff --git a/packages/core/src/layout/__tests__/attribution.test.ts b/packages/core/src/layout/__tests__/attribution.test.ts
new file mode 100644
index 00000000..796972b3
--- /dev/null
+++ b/packages/core/src/layout/__tests__/attribution.test.ts
@@ -0,0 +1,109 @@
+import { describe, expect, it } from 'vitest';
+import { resolveTheme } from '../../theme/resolve';
+import { computeChrome } from '../chrome';
+import { estimateTextHeight } from '../text-measure';
+
+const theme = resolveTheme();
+const measured = (text: string, size: number) => ({
+ width: text.length * size * 0.6,
+ height: size,
+});
+
+describe('measured attribution', () => {
+ it('reserves the measured brand width instead of a fixed 130px column', () => {
+ const chrome = computeChrome({ source: 'Source: Census' }, theme, 600, measured);
+ expect(chrome.watermark?.text).toBe('OpenData');
+ expect(chrome.source!.maxWidth).toBeGreaterThan(600 - theme.spacing.padding * 2 - 130);
+ expect(chrome.source!.x + chrome.source!.maxWidth).toBeLessThan(
+ chrome.watermark!.x - chrome.watermark!.maxWidth,
+ );
+ });
+ it('gives a long narrow source the full row when stacking saves height', () => {
+ const chrome = computeChrome(
+ { source: 'Source: A long research institute and county administrative records, 2020–2025' },
+ theme,
+ 180,
+ measured,
+ );
+ expect(chrome.attributionLayout).toBe('stacked');
+ expect(chrome.source!.maxWidth).toBe(180 - theme.spacing.padding * 2 - 5);
+ expect(chrome.watermark!.y).toBeGreaterThan(chrome.source!.y);
+ });
+ it('keeps short desktop attribution inline', () => {
+ const chrome = computeChrome(
+ { source: 'Source: Census', byline: 'By Research Team' },
+ theme,
+ 1280,
+ measured,
+ );
+ expect(chrome.attributionLayout).toBe('inline');
+ expect(chrome.watermark!.y).toBe(chrome.source!.y);
+ });
+ it('measures multiline custom brands, includes legend reserve, and suppresses watermark', () => {
+ const chrome = computeChrome(
+ { brand: { text: 'Our research\nOpen methods', style: { fontSize: 24 } } },
+ theme,
+ 390,
+ measured,
+ 'full',
+ undefined,
+ true,
+ 44,
+ );
+ expect(chrome.watermark).toBeUndefined();
+ expect(chrome.brand!.y).toBe(44 + theme.spacing.chartToFooter);
+ expect(chrome.bottomHeight).toBeGreaterThanOrEqual(
+ chrome.brand!.y +
+ estimateTextHeight(24, 2, chrome.brand!.style.lineHeight) +
+ theme.spacing.padding,
+ );
+ });
+ it('preserves explicit custom brand style, offsets, and maxLines', () => {
+ const chrome = computeChrome(
+ {
+ source: { text: 'Source', offset: { dx: 3, dy: 4 } },
+ brand: {
+ text: 'Authored brand',
+ offset: { dx: -7, dy: 9 },
+ style: { fontSize: 22, color: 'red', maxLines: 1 },
+ },
+ },
+ theme,
+ 1280,
+ measured,
+ );
+ expect(chrome.source!.x).toBe(theme.spacing.padding + 3);
+ expect(chrome.brand!.x).toBe(1280 - theme.spacing.padding - 7);
+ expect(chrome.brand!.y).toBe(theme.spacing.chartToFooter + 9);
+ expect(chrome.brand!.style.fontSize).toBe(22);
+ expect(chrome.brand!.style.fill).toBe('red');
+ expect(chrome.brand!.maxLines).toBe(1);
+ });
+ it.each([
+ 'compact',
+ 'hidden',
+ ] as const)('%s preserves its existing hidden-attribution policy', (mode) => {
+ const chrome = computeChrome({ source: 'Source', brand: 'Custom' }, theme, 390, measured, mode);
+ expect(chrome.source).toBeUndefined();
+ expect(chrome.brand).toBeUndefined();
+ expect(chrome.watermark?.text).toBe('OpenData');
+ });
+ it('watermark off gives attribution the full width without a ghost reserve', () => {
+ const chrome = computeChrome(
+ { source: 'Source' },
+ theme,
+ 390,
+ measured,
+ 'full',
+ undefined,
+ false,
+ );
+ expect(chrome.source!.maxWidth).toBe(390 - theme.spacing.padding * 2 - 5);
+ expect(chrome.watermark).toBeUndefined();
+ });
+ it('empty chrome still resolves the same portable watermark as other modes', () => {
+ const chrome = computeChrome(undefined, theme, 390, measured);
+ expect(chrome.watermark?.style.fontSize).toBe(12);
+ expect(chrome.watermark?.y).toBe(theme.spacing.chartToFooter);
+ });
+});
diff --git a/packages/core/src/layout/__tests__/chrome.test.ts b/packages/core/src/layout/__tests__/chrome.test.ts
index 58ab9a53..91c83f2d 100644
--- a/packages/core/src/layout/__tests__/chrome.test.ts
+++ b/packages/core/src/layout/__tests__/chrome.test.ts
@@ -143,11 +143,11 @@ describe('computeChrome', () => {
};
const result = computeChrome(chrome, theme, 600);
- expect(result.title).toBeDefined();
- expect(result.subtitle).toBeDefined();
- expect(result.source).toBeDefined();
- expect(result.byline).toBeDefined();
- expect(result.footer).toBeDefined();
+ expect(result.title!.text).toBe('Title');
+ expect(result.subtitle!.text).toBe('Subtitle');
+ expect(result.source!.text).toBe('Source');
+ expect(result.byline!.text).toBe('By Author');
+ expect(result.footer!.text).toBe('Footer note');
expect(result.topHeight).toBeGreaterThan(0);
expect(result.bottomHeight).toBeGreaterThan(0);
});
@@ -242,15 +242,22 @@ describe('computeChrome', () => {
});
it('uses measureText function when provided', () => {
- const measureText = (text: string, fontSize: number) => ({
- width: text.length * fontSize * 0.6,
+ // Same text, two measurers: one says every word is huge, one says tiny.
+ // If the measurer drives wrapping, the huge one wraps onto more lines.
+ const wide = (text: string, fontSize: number) => ({
+ width: text.length * fontSize * 10,
+ height: fontSize * 1.2,
+ });
+ const narrow = (text: string, fontSize: number) => ({
+ width: text.length * fontSize * 0.1,
height: fontSize * 1.2,
});
- const chrome: Chrome = { title: 'Title' };
- const result = computeChrome(chrome, theme, 600, measureText);
- expect(result.title).toBeDefined();
- // Just verify it runs without error; exact values depend on measure fn
+ const chrome: Chrome = { title: 'Quarterly revenue by region' };
+ const wideResult = computeChrome(chrome, theme, 600, wide);
+ const narrowResult = computeChrome(chrome, theme, 600, narrow);
+ expect(wideResult.title!.text).toBe('Quarterly revenue by region');
+ expect(wideResult.topHeight).toBeGreaterThan(narrowResult.topHeight);
});
it('returns zero bottom height when watermark is false and no bottom chrome', () => {
diff --git a/packages/core/src/layout/__tests__/note-band-compat.test.ts b/packages/core/src/layout/__tests__/note-band-compat.test.ts
index 82bfdb1e..2f458921 100644
--- a/packages/core/src/layout/__tests__/note-band-compat.test.ts
+++ b/packages/core/src/layout/__tests__/note-band-compat.test.ts
@@ -1,14 +1,12 @@
-import { expect, it, vi } from 'vitest';
+import { expect, it } from 'vitest';
+import * as publicCore from '../../index';
import { resolveTheme } from '../../theme/resolve';
-import { footnoteBandHeight } from '../chrome';
+import { computeNoteBandHeight } from '../chrome';
-it('warns once and retains the numeric API while preferring resolved band metadata', () => {
- const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
+it('removes the public footnoteBandHeight alias and prefers resolved note-band metadata', () => {
+ expect(publicCore).not.toHaveProperty('footnoteBandHeight');
const theme = resolveTheme();
- expect(footnoteBandHeight(2, theme)).toBeCloseTo(2 * 11 * 1.3 + 4);
- expect(footnoteBandHeight({ noteBandHeight: 87 }, theme)).toBe(87);
- expect(footnoteBandHeight(0, theme)).toBe(0);
- expect(warn).toHaveBeenCalledTimes(1);
- expect(warn.mock.calls[0][0]).toContain('noteBandHeight');
- warn.mockRestore();
+ expect(computeNoteBandHeight({ noteBandHeight: 87 }, theme)).toBe(87);
+ expect(computeNoteBandHeight(2, theme)).toBeCloseTo(2 * theme.fonts.sizes.small * 1.3 + 4);
+ expect(computeNoteBandHeight(0, theme)).toBe(0);
});
diff --git a/packages/core/src/layout/chrome.ts b/packages/core/src/layout/chrome.ts
index 13774402..6d66db3a 100644
--- a/packages/core/src/layout/chrome.ts
+++ b/packages/core/src/layout/chrome.ts
@@ -26,7 +26,6 @@ import type { ChromeDefaults, ResolvedTheme } from '../types/theme';
import {
BRAND_FONT_SIZE,
BRAND_MIN_WIDTH,
- BRAND_RESERVE_WIDTH,
COMPACT_WIDTH,
estimateCharWidth,
estimateTextHeight,
@@ -195,21 +194,31 @@ export function computeNoteBandHeight(
: count * theme.fonts.sizes.small * FOOTNOTE_LINE_HEIGHT + FOOTNOTE_BAND_GAP;
}
-let warnedFootnoteBandHeight = false;
-/**
- * @deprecated Read layout.chrome.noteBandHeight. The numeric signature remains compatible in 8.x.
- */
-export function footnoteBandHeight(
- input: number | Pick,
+function measuredWatermark(
theme: ResolvedTheme,
-): number {
- if (!warnedFootnoteBandHeight) {
- warnedFootnoteBandHeight = true;
- console.warn(
- '[openchart] footnoteBandHeight is deprecated; read layout.chrome.noteBandHeight.',
- );
- }
- return computeNoteBandHeight(input, theme);
+ width: number,
+ pad: number,
+ y: number,
+ measureText?: MeasureTextFn,
+): ResolvedChromeElement | undefined {
+ if (width < BRAND_MIN_WIDTH) return undefined;
+ const style: TextStyle = {
+ fontFamily: theme.fonts.family,
+ fontSize: BRAND_FONT_SIZE,
+ fontWeight: 600,
+ fill: theme.colors.axis,
+ textAnchor: 'end',
+ lineHeight: 1.3,
+ };
+ return {
+ text: 'OpenData',
+ x: width - pad,
+ y,
+ maxWidth:
+ measureText?.('OpenData', BRAND_FONT_SIZE, 600).width ??
+ 'OpenData'.length * estimateCharWidth(BRAND_FONT_SIZE, 600),
+ style,
+ };
}
/**
@@ -259,6 +268,13 @@ export function computeChrome(
return {
topHeight: 0,
bottomHeight: theme.spacing.chartToFooter + brandHeight + hiddenPad + bottomLegendHeight,
+ watermark: measuredWatermark(
+ theme,
+ width,
+ hiddenPad,
+ theme.spacing.chartToFooter + bottomLegendHeight,
+ measureText,
+ ),
};
}
if (bottomLegendHeight > 0) {
@@ -397,156 +413,132 @@ export function computeChrome(
return {
topHeight,
bottomHeight: compactBottom,
+ ...(watermark
+ ? {
+ watermark: measuredWatermark(
+ theme,
+ width,
+ pad,
+ theme.spacing.chartToFooter + bottomLegendHeight,
+ measureText,
+ ),
+ }
+ : {}),
...(ruleElement ? { rule: ruleElement } : {}),
...topElements,
};
}
- // Bottom elements: source, byline, footer, and an optional custom brand
- // (`chrome.brand`). When a custom brand is supplied it suppresses the default
- // OpenData watermark and renders right-anchored on the same baseline.
+ // Attribution is measured as a unit. Compare inline versus stacked using
+ // the same wrapping model as painted text; ties favor inline. Never reduce
+ // an authored font size or remeasure in response to interaction state.
+ const startY = theme.spacing.chartToFooter + bottomLegendHeight;
const brandNorm = normalizeChromeText(chrome.brand);
- const showWatermark = watermark && !brandNorm;
- // Reserve space on the right for the brand watermark or custom brand so the
- // left-anchored bottom chrome items don't overlap.
- const shouldReserveBrandWidth = (showWatermark || !!brandNorm) && width >= BRAND_MIN_WIDTH;
- const bottomMaxWidth = maxWidth - (shouldReserveBrandWidth ? BRAND_RESERVE_WIDTH : 0);
- const bottomElements: Partial> = {};
- let bottomHeight = 0;
-
- const bottomItems: Array<{
- key: 'source' | 'byline' | 'footer';
- norm: { text: string; style?: ChromeText['style']; offset?: ChromeText['offset'] };
- defaults: ChromeDefaults;
- }> = [];
-
- const sourceNorm = normalizeChromeText(chrome.source);
- if (sourceNorm) {
- bottomItems.push({
- key: 'source',
- norm: sourceNorm,
- defaults: theme.chrome.source,
- });
- }
-
- const bylineNorm = normalizeChromeText(chrome.byline);
- if (bylineNorm) {
- bottomItems.push({
- key: 'byline',
- norm: bylineNorm,
- defaults: theme.chrome.byline,
- });
- }
-
- const footerNorm = normalizeChromeText(chrome.footer);
- if (footerNorm) {
- bottomItems.push({
- key: 'footer',
- norm: footerNorm,
- defaults: theme.chrome.footer,
- });
- }
-
- if (bottomItems.length > 0) {
- bottomHeight += theme.spacing.chartToFooter;
- // Push bottom chrome below the bottom legend (if reserved). Stored y values
- // include this offset so renderers don't need to know about the legend band.
- bottomHeight += bottomLegendHeight;
-
- for (const item of bottomItems) {
- const style = buildTextStyle(
- item.defaults,
- fontFamily,
- item.defaults.color,
- width,
- item.norm.style,
- );
- const maxLines = item.norm.style?.maxLines;
- const lineCount = estimateLineCount(
- item.norm.text,
- style,
- bottomMaxWidth,
- measureText,
- maxLines,
- );
- const height = estimateTextHeight(style.fontSize, lineCount, style.lineHeight);
-
- // y positions will be computed relative to the bottom of the
- // chart area by the engine. We store offsets from bottom start.
- bottomElements[item.key] = {
- text: item.norm.text,
- x: pad + (item.norm.offset?.dx ?? 0),
- y: bottomHeight + (item.norm.offset?.dy ?? 0), // offset from where bottom chrome starts
- maxWidth: bottomMaxWidth,
- ...(maxLines != null ? { maxLines } : {}),
- style,
- };
-
- bottomHeight += height + chromeGap;
- }
-
- // Remove trailing gap
- bottomHeight -= chromeGap;
-
- // Ensure bottom height accommodates the brand watermark, which renders
- // at the same Y as the first bottom chrome item but is taller (20px font
- // vs 12px source). Without this, the brand overflows the SVG viewBox.
- if (showWatermark && width >= BRAND_MIN_WIDTH) {
- const brandHeight = estimateTextHeight(BRAND_FONT_SIZE, 1);
- // firstItemY is chartToFooter (the Y offset of the first bottom item).
- // The brand needs brandHeight below that point; bottom chrome content
- // needs (bottomHeight - chartToFooter). Take the max.
- const contentBelowFirstItem = bottomHeight - theme.spacing.chartToFooter;
- if (brandHeight > contentBelowFirstItem) {
- bottomHeight += brandHeight - contentBelowFirstItem;
- }
- }
-
- // Add bottom padding
- bottomHeight += pad;
- } else if (showWatermark && width >= BRAND_MIN_WIDTH) {
- // No bottom chrome items, but brand watermark still renders.
- // Reserve space: chartToFooter gap + brand text height + padding,
- // plus the bottom-legend reservation so the watermark sits below it.
- const brandHeight = estimateTextHeight(BRAND_FONT_SIZE, 1);
- bottomHeight = theme.spacing.chartToFooter + brandHeight + pad + bottomLegendHeight;
- } else if (bottomLegendHeight > 0) {
- // No bottom chrome content and no watermark, but a bottom legend was
- // reserved upstream. Surface the reservation so callers don't need to
- // re-add it on top of bottomHeight.
- bottomHeight = bottomLegendHeight;
- }
-
- // Custom brand is right-anchored on the same row as the first bottom chrome
- // item. Compute its style/position once we know whether bottom chrome exists.
+ const watermarkElement =
+ watermark && !brandNorm ? measuredWatermark(theme, width, pad, startY, measureText) : undefined;
let brandElement: ResolvedChromeElement | undefined;
if (brandNorm && width >= BRAND_MIN_WIDTH) {
- const brandStyle = buildTextStyle(
+ const style = buildTextStyle(
theme.chrome.footer,
fontFamily,
theme.chrome.footer.color,
width,
brandNorm.style,
);
- brandStyle.textAnchor = 'end';
- // Y matches the first bottom item (chartToFooter offset). When there are
- // no bottom items, the brand becomes the sole footer element and we need
- // to allocate the same baseline.
- const brandY =
- bottomItems.length > 0 ? theme.spacing.chartToFooter : theme.spacing.chartToFooter;
+ style.textAnchor = 'end';
+ const naturalWidth = Math.max(
+ ...brandNorm.text
+ .split('\n')
+ .map(
+ (line) =>
+ measureText?.(line, style.fontSize, style.fontWeight).width ??
+ line.length * estimateCharWidth(style.fontSize, style.fontWeight),
+ ),
+ );
brandElement = {
text: brandNorm.text,
x: width - pad + (brandNorm.offset?.dx ?? 0),
- y: brandY + (brandNorm.offset?.dy ?? 0),
- maxWidth: BRAND_RESERVE_WIDTH,
- style: brandStyle,
+ y: startY + (brandNorm.offset?.dy ?? 0),
+ maxWidth: Math.max(1, Math.min(maxWidth, naturalWidth)),
+ ...(brandNorm.style?.maxLines != null ? { maxLines: brandNorm.style.maxLines } : {}),
+ style,
};
- // Ensure bottomHeight reserves space when brand is the only footer item.
- if (bottomItems.length === 0) {
- const brandHeight = estimateTextHeight(brandStyle.fontSize, 1, brandStyle.lineHeight);
- bottomHeight = theme.spacing.chartToFooter + brandHeight + pad;
+ }
+ const brand = brandElement ?? watermarkElement;
+ const brandHeight = brand
+ ? estimateTextHeight(
+ brand.style.fontSize,
+ estimateLineCount(brand.text, brand.style, brand.maxWidth, measureText, brand.maxLines),
+ brand.style.lineHeight,
+ )
+ : 0;
+ const items = (['source', 'byline', 'footer'] as const).flatMap((key) => {
+ const norm = normalizeChromeText(chrome[key]);
+ return norm
+ ? [
+ {
+ key,
+ norm,
+ style: buildTextStyle(
+ theme.chrome[key],
+ fontFamily,
+ theme.chrome[key].color,
+ width,
+ norm.style,
+ ),
+ },
+ ]
+ : [];
+ });
+ const gap = Math.max(12, chromeGap);
+ // Account for authored horizontal movement when deciding fit. Explicit
+ // offsets still win; the planner doesn't silently rewrite their intent.
+ const inlineWidth = brand ? Math.max(0, brand.x - brand.maxWidth - gap - pad) : maxWidth;
+ function resolveItems(availableWidth: number) {
+ const elements: Partial> = {};
+ let y = startY;
+ let extent = startY;
+ for (const { key, norm, style } of items) {
+ const available = Math.max(1, availableWidth - Math.max(0, norm.offset?.dx ?? 0));
+ const count = estimateLineCount(
+ norm.text,
+ style,
+ available,
+ measureText,
+ norm.style?.maxLines,
+ );
+ const height = estimateTextHeight(style.fontSize, count, style.lineHeight);
+ const itemY = y + (norm.offset?.dy ?? 0);
+ elements[key] = {
+ text: norm.text,
+ x: pad + (norm.offset?.dx ?? 0),
+ y: itemY,
+ maxWidth: available,
+ ...(norm.style?.maxLines != null ? { maxLines: norm.style.maxLines } : {}),
+ style,
+ };
+ extent = Math.max(extent, itemY + height);
+ y += height + chromeGap;
}
+ return {
+ elements,
+ extent,
+ height: items.length ? Math.max(y - chromeGap, extent) - startY : 0,
+ };
}
+ const full = resolveItems(maxWidth);
+ const inline = resolveItems(inlineWidth);
+ const inlineHeight = Math.max(inline.height, (brand?.y ?? startY) - startY + brandHeight);
+ const stackedHeight =
+ full.height +
+ (items.length ? chromeGap : 0) +
+ brandHeight +
+ Math.max(0, brandNorm?.offset?.dy ?? 0);
+ const stacked = !!brand && items.length > 0 && (inlineWidth < 40 || stackedHeight < inlineHeight);
+ const bottomElements = stacked ? full.elements : inline.elements;
+ if (stacked && brand) brand.y += full.height + chromeGap;
+ const contentHeight = stacked ? stackedHeight : inlineHeight;
+ const bottomHeight = items.length || brand ? startY + contentHeight + pad : bottomLegendHeight;
return {
topHeight,
@@ -555,5 +547,7 @@ export function computeChrome(
...topElements,
...bottomElements,
...(brandElement ? { brand: brandElement } : {}),
+ ...(watermarkElement ? { watermark: watermarkElement } : {}),
+ ...(items.length || brand ? { attributionLayout: stacked ? 'stacked' : 'inline' } : {}),
};
}
diff --git a/packages/core/src/layout/index.ts b/packages/core/src/layout/index.ts
index 990266b4..6452353c 100644
--- a/packages/core/src/layout/index.ts
+++ b/packages/core/src/layout/index.ts
@@ -6,7 +6,6 @@ export {
computeChrome,
computeNoteBandHeight,
FOOTNOTE_LINE_HEIGHT,
- footnoteBandHeight,
} from './chrome';
export {
BRAND_FONT_SIZE,
diff --git a/packages/core/src/locale/__tests__/format.test.ts b/packages/core/src/locale/__tests__/format.test.ts
index 514404dd..73954eae 100644
--- a/packages/core/src/locale/__tests__/format.test.ts
+++ b/packages/core/src/locale/__tests__/format.test.ts
@@ -353,6 +353,15 @@ describe('defaultNumberFormatter', () => {
const fmt = defaultNumberFormatter({ extent: [-2000, 2000], allIntegers: true });
expect(fmt(2000)).toBe('2k');
});
+
+ it('step below 1000: least precision that tells ticks apart, no trailing zeros', () => {
+ const half = defaultNumberFormatter({ step: 0.5, stepReference: 5 });
+ expect([4, 4.5, 5].map(half)).toEqual(['4', '4.5', '5']);
+ const twentieth = defaultNumberFormatter({ step: 0.05, stepReference: 4.4 });
+ expect([4.1, 4.15, 4.2].map(twentieth)).toEqual(['4.1', '4.15', '4.2']);
+ const tens = defaultNumberFormatter({ step: 50, stepReference: 900 });
+ expect(tens(850)).toBe('850');
+ });
});
describe('buildCompactStepFormatter', () => {
diff --git a/packages/core/src/locale/format.ts b/packages/core/src/locale/format.ts
index e5b64f5b..b9be48e8 100644
--- a/packages/core/src/locale/format.ts
+++ b/packages/core/src/locale/format.ts
@@ -24,6 +24,12 @@ const FORMAT_PERCENT_RAW = d3Format(',.1~f');
const FORMAT_CURRENCY_INTEGER = d3Format('$,');
const FORMAT_CURRENCY_DECIMAL = d3Format('$,.2f');
const FORMAT_STEP_FRACTION = [d3Format('.0~f'), d3Format('.1~f'), d3Format('.2~f')] as const;
+const FORMAT_STEP_PRECISION = [
+ d3Format(',.0~f'),
+ d3Format(',.1~f'),
+ d3Format(',.2~f'),
+ d3Format(',.3~f'),
+] as const;
// ---------------------------------------------------------------------------
// Number formatting
@@ -346,6 +352,16 @@ export function defaultNumberFormatter(ctx?: FieldFormatContext): NumberFormatte
return buildCompactStepFormatter(ctx.step);
}
+ // Uniform ticks below 1000: the step's own precision tells every tick apart,
+ // so 3.5 / 4 / 4.5 read "3.5", "4", "4.5" rather than "3.50" beside "4".
+ if (ctx?.step != null && ctx.step > 0) {
+ const p = fractionDigits(ctx.step);
+ if (p <= 3) {
+ const fmt = FORMAT_STEP_PRECISION[p];
+ return (v: number) => (Number.isFinite(v) ? fmt(v) : String(v));
+ }
+ }
+
return (v: number) => {
if (!Number.isFinite(v)) return String(v);
// When no context is provided (ARIA marks, standalone helpers) we
diff --git a/packages/core/src/responsive/index.ts b/packages/core/src/responsive/index.ts
index f027a253..b112e6b6 100644
--- a/packages/core/src/responsive/index.ts
+++ b/packages/core/src/responsive/index.ts
@@ -39,7 +39,6 @@ export {
HPAD_COMPACT_MIN,
LABEL_GAP_COMPACT,
LABEL_GAP_DEFAULT,
- LABEL_GAP_NARROW_MAX,
MAX_LEFT_LABEL_FRACTION_COMPACT,
MAX_LEFT_LABEL_FRACTION_DEFAULT,
MAX_LEFT_LABEL_FRACTION_MEDIUM,
@@ -49,7 +48,6 @@ export {
TICK_LABEL_OFFSET,
TICK_LINE_HEIGHT_FACTOR,
TOP_PAD_EXTRA_NARROW,
- TOP_PAD_NARROW_MAX,
truncateRotatedLabel,
truncateToWidth,
X_AXIS_BAND_HEIGHT,
diff --git a/packages/core/src/responsive/metrics.ts b/packages/core/src/responsive/metrics.ts
index 4474ace5..532ff569 100644
--- a/packages/core/src/responsive/metrics.ts
+++ b/packages/core/src/responsive/metrics.ts
@@ -129,9 +129,6 @@ export const NARROW_VIEWPORT_MAX = 500;
/** Extra top padding added below NARROW_VIEWPORT_MAX to clear iOS Safari browser chrome. */
export const TOP_PAD_EXTRA_NARROW = 10;
-/** @deprecated Use NARROW_VIEWPORT_MAX instead. */
-export const TOP_PAD_NARROW_MAX = NARROW_VIEWPORT_MAX;
-
// ---------------------------------------------------------------------------
// Category label gaps (left-axis bar/dot charts)
// ---------------------------------------------------------------------------
@@ -142,9 +139,6 @@ export const LABEL_GAP_COMPACT = 8;
/** Gap between category label text and chart edge on standard viewports. */
export const LABEL_GAP_DEFAULT = 12;
-/** @deprecated Use NARROW_VIEWPORT_MAX instead. */
-export const LABEL_GAP_NARROW_MAX = NARROW_VIEWPORT_MAX;
-
// ---------------------------------------------------------------------------
// Max left-axis label fraction
// ---------------------------------------------------------------------------
diff --git a/packages/core/src/schema/__tests__/schema.test.ts b/packages/core/src/schema/__tests__/schema.test.ts
index d6395c32..11301e9c 100644
--- a/packages/core/src/schema/__tests__/schema.test.ts
+++ b/packages/core/src/schema/__tests__/schema.test.ts
@@ -219,3 +219,70 @@ describe('table.schema.json', () => {
expect(validateTable(spec)).toBe(false);
});
});
+
+describe('geographic label authoring schema', () => {
+ const map = {
+ type: 'map',
+ geo: { features: {}, projection: 'identity' },
+ data: [{ id: 'area', caption: 'Authored name', score: 10 }],
+ encoding: {
+ key: { field: 'id', type: 'nominal' },
+ color: { field: 'score', type: 'quantitative' },
+ },
+ };
+ it('keeps label-free maps valid and accepts explicit text channels and typed callout anchors', () => {
+ expect(validateViz(map)).toBe(true);
+ for (const anchor of [
+ { kind: 'feature', id: 'area' },
+ { kind: 'point', id: 1 },
+ { kind: 'coordinate', space: 'source', x: 10, y: 20 },
+ { kind: 'coordinate', space: 'geographic', x: -97, y: 30 },
+ ]) {
+ const valid = validateViz({
+ ...map,
+ encoding: {
+ ...map.encoding,
+ text: { field: 'caption', type: 'nominal' },
+ textPriority: { field: 'score', type: 'quantitative' },
+ },
+ callouts: [
+ {
+ id: 'finding',
+ anchor,
+ text: 'A finding',
+ subtitle: 'Exact authored text',
+ minZoom: 1,
+ maxZoom: 4,
+ maxWidth: 240,
+ offset: { dx: 0, dy: 0 },
+ },
+ ],
+ });
+ expect(valid, JSON.stringify(validateViz.errors)).toBe(true);
+ }
+ });
+ it('rejects malformed coordinate and entity anchors instead of accepting an extension bag', () => {
+ for (const anchor of [
+ { kind: 'coordinate', space: 'pixels', x: 0, y: 0 },
+ { kind: 'coordinate', space: 'source', x: 10 },
+ { kind: 'feature', id: {} },
+ { kind: 'unknown', id: 'area' },
+ ])
+ expect(
+ validateViz({ ...map, callouts: [{ id: 'finding', anchor, text: 'A finding' }] }),
+ ).toBe(false);
+ });
+ it('does not introduce geographic callouts into Cartesian charts', () => {
+ expect(
+ validateChart({
+ mark: 'bar',
+ data: sampleData,
+ encoding: {
+ x: { field: 'cat', type: 'nominal' },
+ y: { field: 'amount', type: 'quantitative' },
+ },
+ callouts: [{ id: 'finding', anchor: { kind: 'feature', id: 'area' }, text: 'A finding' }],
+ }),
+ ).toBe(false);
+ });
+});
diff --git a/packages/core/src/styles/__tests__/token-theme-parity.test.ts b/packages/core/src/styles/__tests__/token-theme-parity.test.ts
index eb3b06ae..3fd53adf 100644
--- a/packages/core/src/styles/__tests__/token-theme-parity.test.ts
+++ b/packages/core/src/styles/__tests__/token-theme-parity.test.ts
@@ -8,11 +8,12 @@
* counterpart must equal it in both modes.
*/
+import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
import { adaptTheme } from '../../theme/dark-mode';
import { DEFAULT_THEME } from '../../theme/defaults';
import { resolveTheme } from '../../theme/resolve';
-import { cssTokenDefault } from '../token-definitions';
+import { CSS_TOKENS, cssTokenDefault } from '../token-definitions';
/** `rgba(0, 0, 0, 0.08)` and `rgba(0,0,0,0.08)` are the same color. */
const norm = (v: string) => v.replace(/\s+/g, '').toLowerCase();
@@ -118,3 +119,10 @@ it('annotation tokens preserve both legacy modes and authored pairs', () => {
expect(adaptTheme(custom).colors[name]).toBe('#abcdef');
}
});
+
+it('emits every registered dark token in the generated stylesheet', () => {
+ const css = readFileSync(new URL('../dark.css', import.meta.url), 'utf8');
+ for (const token of CSS_TOKENS) {
+ if (token.dark !== undefined) expect(css).toContain(`${token.name}: ${token.dark};`);
+ }
+});
diff --git a/packages/core/src/styles/base.css b/packages/core/src/styles/base.css
index ed7dead9..6717984b 100644
--- a/packages/core/src/styles/base.css
+++ b/packages/core/src/styles/base.css
@@ -63,6 +63,8 @@
packages/vanilla/src/dom-helpers.ts for why. Keep the two in sync. */
.oc-sr-only {
position: absolute;
+ top: 0;
+ left: 0;
width: 1px;
height: 1px;
padding: 0;
diff --git a/packages/core/src/styles/dark.css b/packages/core/src/styles/dark.css
index 7a60a0f5..803ec30d 100644
--- a/packages/core/src/styles/dark.css
+++ b/packages/core/src/styles/dark.css
@@ -4,9 +4,8 @@
/* ---------------------------------------------------------------------------
* Dark mode overrides (fallback defaults)
*
- * At runtime, the JS theme engine stamps dark-adapted --oc-* custom
- * properties on each .oc-root container. These CSS values serve as
- * fallbacks for contexts where JS hasn't mounted yet.
+ * Dark defaults stay in CSS; authored theme overrides are resolved and
+ * stamped by the JS theme engine.
* --------------------------------------------------------------------------- */
.oc-dark {
@@ -40,5 +39,25 @@
--oc-tooltip-shadow: 0 2px 8px rgba(0, 0, 0, 0.3), 0 0 1px rgba(0, 0, 0, 0.4);
--oc-tooltip-text: #f7f8f8;
--oc-legend-text: #d0d6e0;
+
+ /* Annotation and neutral colors */
+ --oc-gridline-zero: rgba(255, 255, 255, 0.24);
+ --oc-map-no-data-fill: #171719;
+ --oc-map-no-data-hatch: #3d3e3f;
+ --oc-map-interior-border: rgba(255, 255, 255, 0.18);
+ --oc-map-outline-border: rgba(255, 255, 255, 0.38);
+ --oc-annotation-label: #d1d5db;
+ --oc-annotation-muted: #9ca3af;
+ --oc-annotation-leader: #9aa0a6;
+ --oc-annotation-rule: #9ca3af;
+ --oc-annotation-range-fill: #f0c040;
+ --oc-annotation-marker-fill: #09090b;
+ --oc-annotation-plate: #09090b;
+ --oc-gray-100: #171719;
+ --oc-gray-200: #262627;
+ --oc-gray-300: #3d3e3f;
+ --oc-gray-400: #68696a;
+ --oc-gray-600: #8c8c8d;
+ --oc-gray-800: #bcbcbd;
}
}
diff --git a/packages/core/src/styles/index.css b/packages/core/src/styles/index.css
index d36ece9d..3653cfa7 100644
--- a/packages/core/src/styles/index.css
+++ b/packages/core/src/styles/index.css
@@ -17,6 +17,7 @@
@import "./chrome.css";
@import "./tooltip.css";
@import "./legend.css";
+@import "./reader.css";
@import "./interaction.css";
@import "./series-search.css";
@import "./you-draw-it.css";
diff --git a/packages/core/src/styles/interaction.css b/packages/core/src/styles/interaction.css
index be4c5037..d542acd3 100644
--- a/packages/core/src/styles/interaction.css
+++ b/packages/core/src/styles/interaction.css
@@ -135,3 +135,17 @@
stroke-width: 1.5;
}
}
+
+@layer oc.components {
+ /* Source-linked annotation inspection; exports render a fresh, inactive scene. */
+ .oc-footnotes,
+ [data-note-marker-index] {
+ cursor: pointer;
+ }
+ .oc-footnotes.oc-note-active {
+ text-decoration: underline;
+ }
+ [data-note-marker-index].oc-note-active .oc-annotation-footnote-marker {
+ stroke-width: 3;
+ }
+}
diff --git a/packages/core/src/styles/reader.css b/packages/core/src/styles/reader.css
new file mode 100644
index 00000000..18f95770
--- /dev/null
+++ b/packages/core/src/styles/reader.css
@@ -0,0 +1,140 @@
+@layer oc.components {
+ .oc-reader-panel,
+ .oc-map-companion {
+ color: var(--oc-text);
+ font-family: var(--oc-font-family);
+ font-size: 14px;
+ line-height: 1.5;
+ min-width: 0;
+ }
+
+ .oc-reader-panel {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
+ gap: 16px;
+ padding: 16px 0;
+ border-block: 1px solid var(--oc-border);
+ }
+
+ .oc-reader-panel article {
+ min-width: 0;
+ padding-inline-end: 16px;
+ overflow-wrap: anywhere;
+ }
+
+ .oc-reader-panel h3 {
+ margin: 0 0 6px;
+ font-size: 15px;
+ line-height: 1.4;
+ font-weight: 600;
+ }
+
+ .oc-reader-panel p {
+ margin: 0 0 8px;
+ color: var(--oc-text-secondary);
+ }
+
+ .oc-reader-panel dl {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ gap: 6px 16px;
+ margin: 12px 0;
+ font-variant-numeric: tabular-nums;
+ }
+
+ .oc-reader-panel dt {
+ color: var(--oc-text-muted);
+ }
+ .oc-reader-panel dd {
+ margin: 0;
+ text-align: end;
+ }
+ .oc-reader-empty,
+ .oc-reader-comparison {
+ grid-column: 1 / -1;
+ }
+ .oc-reader-panel > button {
+ justify-self: start;
+ align-self: end;
+ }
+
+ .oc-reader-panel button,
+ .oc-map-companion button,
+ .oc-map-inspect-places {
+ font: inherit;
+ font-size: 13px;
+ color: var(--oc-text-secondary);
+ background: transparent;
+ border: 1px solid var(--oc-border);
+ border-radius: 5px;
+ padding: 6px 10px;
+ min-height: 36px;
+ cursor: pointer;
+ margin-inline-end: 6px;
+ margin-block: 3px;
+ white-space: normal;
+ text-align: start;
+ }
+
+ .oc-reader-panel button:hover,
+ .oc-map-companion button:hover,
+ .oc-map-inspect-places:hover {
+ background: var(--oc-hover-bg);
+ }
+ .oc-reader-panel button:focus-visible,
+ .oc-map-companion :is(button, input):focus-visible,
+ .oc-map-inspect-places:focus-visible {
+ outline: 2px solid var(--oc-focus);
+ outline-offset: 2px;
+ }
+
+ .oc-map-companion input {
+ width: 100%;
+ box-sizing: border-box;
+ padding: 10px 12px;
+ border: 1px solid var(--oc-border);
+ border-radius: 5px;
+ background: transparent;
+ color: var(--oc-text);
+ font: inherit;
+ }
+
+ .oc-map-companion ol {
+ margin: 12px 0;
+ padding: 0;
+ list-style: none;
+ }
+ .oc-map-companion li {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: 12px;
+ padding-block: 6px;
+ border-bottom: 1px solid var(--oc-border);
+ }
+ .oc-map-companion li > button:first-child {
+ flex: 1 1 auto;
+ border-color: transparent;
+ padding-inline-start: 0;
+ font-variant-numeric: tabular-nums;
+ }
+ .oc-map-companion button:disabled {
+ opacity: 0.45;
+ cursor: default;
+ }
+ .oc-map-inspect-places {
+ position: absolute;
+ inset-inline-start: 8px;
+ top: 8px;
+ }
+ .oc-map-inspect-places:not(:focus-visible) {
+ width: 1px;
+ height: 1px;
+ min-height: 0;
+ padding: 0;
+ border: 0;
+ overflow: hidden;
+ clip-path: inset(50%);
+ white-space: nowrap;
+ }
+}
diff --git a/packages/core/src/styles/table.css b/packages/core/src/styles/table.css
index 1ea30da6..f968018d 100644
--- a/packages/core/src/styles/table.css
+++ b/packages/core/src/styles/table.css
@@ -6,7 +6,6 @@
.oc-table-wrapper {
font-family: var(--oc-font-family);
color: var(--oc-text);
- background: var(--oc-bg);
& > .oc-chrome {
margin-bottom: 16px;
@@ -51,7 +50,6 @@
position: sticky;
top: 0;
z-index: 2;
- background: var(--oc-bg);
}
/* A hairline, like every other rule in the table. The old 2px header rule
@@ -199,7 +197,7 @@
border-radius: var(--oc-radius-md);
font-size: 13px;
font-family: inherit;
- background: var(--oc-bg);
+ background: transparent;
color: var(--oc-text);
box-sizing: border-box;
transition: border-color 0.15s;
@@ -242,7 +240,7 @@
padding: 6px 14px;
border: 1px solid var(--oc-border);
border-radius: var(--oc-radius-md);
- background: var(--oc-bg);
+ background: transparent;
color: var(--oc-text);
cursor: pointer;
font-size: 13px;
@@ -295,6 +293,12 @@
pointer-events: none;
}
+ /* A low-alpha tint of a mid-dark color all but disappears on a dark
+ surface, so dark mode lifts it. */
+ .oc-dark .oc-table-bar-fill {
+ opacity: 0.4;
+ }
+
.oc-table-bar-value {
position: relative;
z-index: 1;
@@ -358,12 +362,10 @@
/* ---------------------------------------------------------------------------
* Density
*
- * regular 48px rows (base), condensed 40px, relaxed 56px. `.oc-table--compact`
- * is the deprecated alias for condensed, kept one release.
+ * regular 48px rows (base), condensed 40px, relaxed 56px.
* --------------------------------------------------------------------------- */
- .oc-table--condensed,
- .oc-table--compact {
+ .oc-table--condensed {
& th,
& td {
padding: 8px 12px;
@@ -465,7 +467,6 @@
position: sticky;
bottom: 0;
z-index: 2;
- background: var(--oc-bg);
& td {
font-weight: 600;
diff --git a/packages/core/src/styles/token-definitions.ts b/packages/core/src/styles/token-definitions.ts
index 28502b16..7881ae76 100644
--- a/packages/core/src/styles/token-definitions.ts
+++ b/packages/core/src/styles/token-definitions.ts
@@ -70,16 +70,62 @@ export const CSS_TOKENS: readonly CssTokenDef[] = [
{ name: '--oc-title-tracking', light: '-0.022em' },
{ name: '--oc-subtitle-size', light: '14px' },
{ name: '--oc-subtitle-weight', light: '400' },
- { name: '--oc-source-size', light: '11px' },
+ { name: '--oc-source-size', light: '12px' },
{ name: '--oc-source-weight', light: '400' },
{ name: '--oc-body-size', light: '13px' },
+ {
+ name: '--oc-gridline-zero',
+ light: 'rgba(0, 0, 0, 0.22)',
+ dark: 'rgba(255, 255, 255, 0.24)',
+ comment: 'Emphasis for existing zero rules; does not change the domain',
+ },
+ {
+ name: '--oc-endpoint-label-size',
+ light: '13px',
+ consumers: 'Endpoint geometry and authored theme stamp',
+ },
+ {
+ name: '--oc-endpoint-value-size',
+ light: '12px',
+ consumers: 'Endpoint geometry and authored theme stamp',
+ },
+ { name: '--oc-endpoint-entry-gap', light: '6px', consumers: 'Endpoint collision spacing' },
+ { name: '--oc-endpoint-value-gap', light: '4px', consumers: 'Endpoint internal spacing' },
+ {
+ name: '--oc-endpoint-column-gap',
+ light: '18px',
+ consumers: 'Endpoint frame reservation and placement',
+ },
+ {
+ name: '--oc-map-no-data-fill',
+ light: '#f0f0f0',
+ dark: '#171719',
+ comment: 'Engine omission derives neutral[100] from the active theme',
+ },
+ {
+ name: '--oc-map-no-data-hatch',
+ light: '#c9c9c9',
+ dark: '#3d3e3f',
+ comment: 'Engine omission derives neutral[300] from the active theme',
+ },
+ {
+ name: '--oc-map-interior-border',
+ light: 'rgba(0, 0, 0, 0.14)',
+ dark: 'rgba(255, 255, 255, 0.18)',
+ },
+ {
+ name: '--oc-map-outline-border',
+ light: 'rgba(0, 0, 0, 0.4)',
+ dark: 'rgba(255, 255, 255, 0.38)',
+ },
+
{ name: '--oc-annotation-label', light: '#333333', dark: '#d1d5db' },
{ name: '--oc-annotation-muted', light: '#6b7280', dark: '#9ca3af' },
{ name: '--oc-annotation-leader', light: '#8f8f8f', dark: '#9aa0a6' },
- { name: '--oc-annotation-rule', light: '#666666', dark: '#666666' },
- { name: '--oc-annotation-range-fill', light: '#000000', dark: '#000000' },
- { name: '--oc-annotation-marker-fill', light: '#ffffff', dark: '#0a0a0a' },
- { name: '--oc-annotation-plate', light: '#ffffff', dark: '#0a0a0a' },
+ { name: '--oc-annotation-rule', light: '#888888', dark: '#9ca3af' },
+ { name: '--oc-annotation-range-fill', light: '#f0c040', dark: '#f0c040' },
+ { name: '--oc-annotation-marker-fill', light: '#ffffff', dark: '#09090b' },
+ { name: '--oc-annotation-plate', light: '#ffffff', dark: '#09090b' },
{ name: '--oc-eyebrow-size', light: '11px' },
{ name: '--oc-eyebrow-weight', light: '500' },
{ name: '--oc-eyebrow-tracking', light: '0.08em' },
@@ -131,7 +177,7 @@ export const CSS_TOKENS: readonly CssTokenDef[] = [
{
section: 'Lines',
name: '--oc-gridline',
- light: 'rgba(0, 0, 0, 0.08)',
+ light: 'rgba(0, 0, 0, 0.05)',
dark: 'rgba(255, 255, 255, 0.06)',
},
{
diff --git a/packages/core/src/styles/tokens.css b/packages/core/src/styles/tokens.css
index bba32d60..4fb1f8fe 100644
--- a/packages/core/src/styles/tokens.css
+++ b/packages/core/src/styles/tokens.css
@@ -4,11 +4,8 @@
/* ---------------------------------------------------------------------------
* Custom properties (light mode defaults)
*
- * These are FALLBACK defaults. At runtime, the JS theme engine stamps the
- * resolved theme as inline --oc-* custom properties on each .oc-root
- * container (see packages/vanilla/src/theme-tokens.ts). These CSS values
- * serve as fallbacks for contexts where JS hasn't mounted yet (SSR, static
- * HTML examples).
+ * Defaults remain in CSS so host styles can override them. The JS theme
+ * engine stamps only authored theme overrides (theme-tokens.ts).
* --------------------------------------------------------------------------- */
.oc-root,
@@ -95,14 +92,25 @@
--oc-title-tracking: -0.022em;
--oc-subtitle-size: 14px;
--oc-subtitle-weight: 400;
- --oc-source-size: 11px;
+ --oc-source-size: 12px;
--oc-source-weight: 400;
--oc-body-size: 13px;
+ /* Emphasis for existing zero rules; does not change the domain */
+ --oc-gridline-zero: rgba(0, 0, 0, 0.22);
+ --oc-endpoint-label-size: 13px;
+ --oc-endpoint-value-size: 12px;
+ --oc-endpoint-entry-gap: 6px;
+ --oc-endpoint-value-gap: 4px;
+ --oc-endpoint-column-gap: 18px;
+ --oc-map-no-data-fill: #f0f0f0; /* Engine omission derives neutral[100] from the active theme */
+ --oc-map-no-data-hatch: #c9c9c9; /* Engine omission derives neutral[300] from the active theme */
+ --oc-map-interior-border: rgba(0, 0, 0, 0.14);
+ --oc-map-outline-border: rgba(0, 0, 0, 0.4);
--oc-annotation-label: #333333;
--oc-annotation-muted: #6b7280;
--oc-annotation-leader: #8f8f8f;
- --oc-annotation-rule: #666666;
- --oc-annotation-range-fill: #000000;
+ --oc-annotation-rule: #888888;
+ --oc-annotation-range-fill: #f0c040;
--oc-annotation-marker-fill: #ffffff;
--oc-annotation-plate: #ffffff;
--oc-eyebrow-size: 11px;
@@ -146,7 +154,7 @@
--oc-gray-800: #474748;
/* Lines */
- --oc-gridline: rgba(0, 0, 0, 0.08);
+ --oc-gridline: rgba(0, 0, 0, 0.05);
--oc-axis: rgba(0, 0, 0, 0.14); /* hairline = theme.colors.hairline, not the tick-label ink */
--oc-border: rgba(0, 0, 0, 0.08);
--oc-border-radius: 8px; /* containers and tooltips; marks use --oc-radius-sm */
diff --git a/packages/core/src/theme/__tests__/chart-map-tokens.test.ts b/packages/core/src/theme/__tests__/chart-map-tokens.test.ts
new file mode 100644
index 00000000..774abad6
--- /dev/null
+++ b/packages/core/src/theme/__tests__/chart-map-tokens.test.ts
@@ -0,0 +1,89 @@
+import { describe, expect, it } from 'vitest';
+import { adaptTheme } from '../dark-mode';
+import { resolveTheme } from '../resolve';
+
+describe('endpoint and map theme controls', () => {
+ it('retains the current endpoint geometry by default', () => {
+ const theme = resolveTheme();
+ expect(theme.fonts.sizes).toMatchObject({ endpointLabel: 13, endpointValue: 12 });
+ expect(theme.spacing).toMatchObject({
+ endpointEntryGap: 6,
+ endpointValueGap: 4,
+ endpointColumnGap: 18,
+ });
+ });
+ it('resolves independently authored sizes and gaps', () => {
+ const theme = resolveTheme({
+ fonts: { sizes: { endpointLabel: 15, endpointValue: 14 } },
+ spacing: { endpointEntryGap: 8, endpointValueGap: 5, endpointColumnGap: 22 },
+ });
+ expect(theme.fonts.sizes.endpointLabel).toBe(15);
+ expect(theme.spacing.endpointColumnGap).toBe(22);
+ });
+ it('leaves unauthored map colors available for derived warm and dark fallbacks', () => {
+ for (const theme of [
+ resolveTheme(),
+ resolveTheme({ colors: { background: '#faf8f5' } }),
+ adaptTheme(resolveTheme()),
+ ]) {
+ expect(theme.colors.mapNoDataFill).toBeUndefined();
+ expect(theme.colors.mapNoDataHatch).toBeUndefined();
+ }
+ });
+ it('honors explicit map light/dark pairs and plain colors', () => {
+ const theme = resolveTheme({
+ colors: {
+ mapNoDataFill: { light: '#f0e0d0', dark: '#102030' },
+ mapNoDataHatch: { light: '#c0b0a0', dark: '#405060' },
+ mapInteriorBorder: '#abcdef',
+ mapOutlineBorder: { light: '#aabbcc', dark: '#ddeeff' },
+ },
+ });
+ expect(theme.colors.mapNoDataFill).toBe('#f0e0d0');
+ const dark = adaptTheme(theme);
+ expect(dark.colors.mapNoDataFill).toBe('#102030');
+ expect(dark.colors.mapNoDataHatch).toBe('#405060');
+ expect(dark.colors.mapInteriorBorder).toBe('#abcdef');
+ expect(dark.colors.mapOutlineBorder).toBe('#ddeeff');
+ });
+});
+
+it('uses approved hierarchy defaults and honors authored mode pairs', () => {
+ expect(resolveTheme().colors).toMatchObject({
+ gridline: 'rgba(0,0,0,0.05)',
+ gridlineZero: 'rgba(0,0,0,0.22)',
+ mapInteriorBorder: 'rgba(0,0,0,0.14)',
+ mapOutlineBorder: 'rgba(0,0,0,0.40)',
+ });
+ expect(adaptTheme(resolveTheme()).colors).toMatchObject({
+ gridline: 'rgba(255,255,255,0.06)',
+ gridlineZero: 'rgba(255,255,255,0.24)',
+ mapInteriorBorder: 'rgba(255,255,255,0.18)',
+ mapOutlineBorder: 'rgba(255,255,255,0.38)',
+ });
+ const theme = resolveTheme({
+ colors: { gridlineZero: { light: 'rgba(0,0,0,0.22)', dark: 'rgba(255,255,255,0.24)' } },
+ });
+ expect(theme.colors.gridlineZero).toBe('rgba(0,0,0,0.22)');
+ expect(adaptTheme(theme).colors.gridlineZero).toBe('rgba(255,255,255,0.24)');
+ expect(
+ adaptTheme(resolveTheme({ colors: { gridlineZero: '#123456' } })).colors.gridlineZero,
+ ).toBe('#123456');
+});
+
+it('preserves explicit plain hierarchy colors even when equal to light defaults', () => {
+ const colors = {
+ gridlineZero: 'rgba(0,0,0,0.22)',
+ mapInteriorBorder: 'rgba(0,0,0,0.14)',
+ mapOutlineBorder: 'rgba(0,0,0,0.40)',
+ };
+ expect(adaptTheme(resolveTheme({ colors })).colors).toMatchObject(colors);
+});
+
+it('keeps new default roles visible on a dark authored canvas without a mode flag', () => {
+ const theme = resolveTheme({ colors: { background: '#121212', text: '#eeeeee' } });
+ expect(theme.colors.gridlineZero?.replaceAll(' ', '')).toBe('rgba(255,255,255,0.24)');
+ expect(theme.colors.mapInteriorBorder?.replaceAll(' ', '')).toBe('rgba(255,255,255,0.18)');
+ expect(theme.colors.mapOutlineBorder?.replaceAll(' ', '')).toBe('rgba(255,255,255,0.38)');
+ expect(theme.colors.mapNoDataHatch).toBeUndefined();
+});
diff --git a/packages/core/src/theme/__tests__/config-parity.test.ts b/packages/core/src/theme/__tests__/config-parity.test.ts
index c1aadb3a..49fb3747 100644
--- a/packages/core/src/theme/__tests__/config-parity.test.ts
+++ b/packages/core/src/theme/__tests__/config-parity.test.ts
@@ -5,13 +5,18 @@ import { mergeThemeConfig } from '../merge';
describe('authored theme contract', () => {
it('covers every resolved theme key except runtime metadata', () => {
- type Missing = Exclude;
+ type Missing = Exclude<
+ keyof ResolvedTheme,
+ keyof ThemeConfig | 'isDark' | '_tokenPairs' | '_authored'
+ >;
expectTypeOf().toEqualTypeOf();
});
- it('keeps v8 shallow precedence and does not mutate either layer', () => {
+ it('keeps nested precedence and does not mutate either layer', () => {
const base = { fonts: { family: 'Base', sizes: { title: 30 } } };
const override = { fonts: { family: 'Override' } };
- expect(mergeThemeConfig(base, override)).toEqual(override);
+ expect(mergeThemeConfig(base, override)).toEqual({
+ fonts: { family: 'Override', sizes: { title: 30 } },
+ });
expect(base.fonts.sizes.title).toBe(30);
expect(mergeThemeConfig(base, undefined)).toBe(base);
});
diff --git a/packages/core/src/theme/__tests__/dark-mode.test.ts b/packages/core/src/theme/__tests__/dark-mode.test.ts
index f7d85049..59b6c79f 100644
--- a/packages/core/src/theme/__tests__/dark-mode.test.ts
+++ b/packages/core/src/theme/__tests__/dark-mode.test.ts
@@ -142,7 +142,7 @@ describe('adaptTheme', () => {
it('preserves explicit chrome color overrides in dark mode', () => {
const light = resolveTheme({
colors: { background: '#ffffff' },
- chrome: { subtitle: '#d0d6e0', source: '#d0d6e0' },
+ chrome: { subtitle: { color: '#d0d6e0' }, source: { color: '#d0d6e0' } },
});
const dark = adaptTheme(light);
expect(dark.chrome.subtitle.color).toBe('#d0d6e0');
@@ -222,7 +222,7 @@ describe('adaptTheme', () => {
expect(dark.chrome.eyebrow.color).toBe('#ff6b64');
// Without a pair, the eyebrow keeps its accent tint across modes.
- const noPair = adaptTheme(resolveTheme({ chrome: { eyebrow: '#e3120b' } }));
+ const noPair = adaptTheme(resolveTheme({ chrome: { eyebrow: { color: '#e3120b' } } }));
expect(noPair.chrome.eyebrow.color).toBe('#e3120b');
});
@@ -233,6 +233,9 @@ describe('adaptTheme', () => {
const dark = adaptTheme(light);
// Text should be adapted to a light color for dark bg readability
expect(dark.colors.text).not.toBe(light.colors.text);
- expect(dark.colors.text).toBeDefined();
+ // ...lighter: more contrast against black than the light-mode text has.
+ expect(contrastRatio(dark.colors.text, '#000000')).toBeGreaterThan(
+ contrastRatio(light.colors.text, '#000000'),
+ );
});
});
diff --git a/packages/core/src/theme/__tests__/defaults.test.ts b/packages/core/src/theme/__tests__/defaults.test.ts
index cb731ab2..ac14323f 100644
--- a/packages/core/src/theme/__tests__/defaults.test.ts
+++ b/packages/core/src/theme/__tests__/defaults.test.ts
@@ -27,8 +27,8 @@ describe('DEFAULT_THEME', () => {
expect(DEFAULT_THEME.chrome.subtitle.fontWeight).toBe(400);
});
- it('source is 11px normal weight', () => {
- expect(DEFAULT_THEME.chrome.source.fontSize).toBe(11);
+ it('source is 12px normal weight', () => {
+ expect(DEFAULT_THEME.chrome.source.fontSize).toBe(12);
expect(DEFAULT_THEME.chrome.source.fontWeight).toBe(400);
});
diff --git a/packages/core/src/theme/__tests__/presets.test.ts b/packages/core/src/theme/__tests__/presets.test.ts
index f201b771..44efdd91 100644
--- a/packages/core/src/theme/__tests__/presets.test.ts
+++ b/packages/core/src/theme/__tests__/presets.test.ts
@@ -7,6 +7,7 @@ import { contrastRatio, meetsAA } from '../../colors/contrast';
import { CATEGORICAL_FILL_PALETTE_DARK, CATEGORICAL_PALETTE_DARK } from '../../colors/palettes';
import { adaptTheme } from '../dark-mode';
import { DEFAULT_THEME } from '../defaults';
+import { mergeThemeConfig } from '../merge';
import { broadsheet, terminal } from '../presets';
import { resolveTheme } from '../resolve';
@@ -96,4 +97,16 @@ describe('theme rule surface', () => {
// No TokenValue pair, so dark mode leaves the authored color alone.
expect(adaptTheme(t).rule?.color).toBe('#123456');
});
+
+ it('adapts a rule color derived from text ink in dark mode', () => {
+ const dark = adaptTheme(resolveTheme({ rule: { width: 80 } }));
+ expect(dark.rule).toEqual({ color: dark.colors.text, width: 80, thickness: 3 });
+ expect(dark.rule?.color).not.toBe('#09090b');
+ const custom = adaptTheme(resolveTheme({ colors: { text: '#333333' }, rule: { width: 8 } }));
+ expect(custom.rule?.color).toBe('#333333');
+ // A preset's authored rule color is unchanged by a partial override.
+ expect(
+ adaptTheme(resolveTheme(mergeThemeConfig(broadsheet, { rule: { width: 80 } })!)).rule?.color,
+ ).toBe(adaptTheme(resolveTheme(broadsheet)).rule?.color);
+ });
});
diff --git a/packages/core/src/theme/__tests__/resolve.test.ts b/packages/core/src/theme/__tests__/resolve.test.ts
index 0e531443..2e0ef40d 100644
--- a/packages/core/src/theme/__tests__/resolve.test.ts
+++ b/packages/core/src/theme/__tests__/resolve.test.ts
@@ -43,7 +43,7 @@ describe('resolveTheme', () => {
});
it('overrides a chrome element color, preserving its size and weight', () => {
- const resolved = resolveTheme({ chrome: { subtitle: '#d0d6e0' } });
+ const resolved = resolveTheme({ chrome: { subtitle: { color: '#d0d6e0' } } });
expect(resolved.chrome.subtitle.color).toBe('#d0d6e0');
// Non-color chrome fields come from the typography scale, untouched.
expect(resolved.chrome.subtitle.fontSize).toBe(14);
@@ -151,12 +151,12 @@ describe('resolveTheme deep merge edge cases', () => {
it('dark background adapts chrome colors without losing chrome structure', () => {
const resolved = resolveTheme({ colors: { background: '#111111', text: '#ffffff' } });
expect(resolved.isDark).toBe(true);
- // Chrome structure should still be fully populated
- expect(resolved.chrome.title).toBeDefined();
- expect(resolved.chrome.subtitle).toBeDefined();
- expect(resolved.chrome.source).toBeDefined();
- expect(resolved.chrome.byline).toBeDefined();
- expect(resolved.chrome.footer).toBeDefined();
+ // Chrome structure should still be fully populated: dark mode adapts colors,
+ // not type sizes.
+ for (const key of ['title', 'subtitle', 'source', 'byline', 'footer'] as const) {
+ expect(resolved.chrome[key].fontSize).toBe(DEFAULT_THEME.chrome[key].fontSize);
+ expect(resolved.chrome[key].color).toMatch(/^#[0-9a-f]{3,8}$/i);
+ }
// Title color should be adapted (not the light-mode default)
expect(resolved.chrome.title.color).not.toBe(DEFAULT_THEME.chrome.title.color);
});
diff --git a/packages/core/src/theme/__tests__/v9-contracts.test.ts b/packages/core/src/theme/__tests__/v9-contracts.test.ts
new file mode 100644
index 00000000..a9dec5cb
--- /dev/null
+++ b/packages/core/src/theme/__tests__/v9-contracts.test.ts
@@ -0,0 +1,81 @@
+import { describe, expect, it } from 'vitest';
+import { adaptTheme } from '../dark-mode';
+import { mergeThemeConfig } from '../merge';
+import { broadsheet, clarity, terminal } from '../presets';
+import { resolveTheme } from '../resolve';
+
+describe('v9 authored theme contract', () => {
+ it('merges nested authored layers without mutating either input', () => {
+ const base = {
+ colors: { background: { light: '#fff', dark: '#000' }, categorical: ['red'] },
+ fonts: { sizes: { small: 15, title: 30 } },
+ };
+ const override = { colors: { background: 'beige' }, fonts: { sizes: { title: 24 } } };
+ expect(mergeThemeConfig(base, override)).toEqual({
+ colors: { background: 'beige', categorical: ['red'] },
+ fonts: { sizes: { small: 15, title: 24 } },
+ });
+ expect(base.fonts.sizes.title).toBe(30);
+ expect(override.fonts.sizes).toEqual({ title: 24 });
+ });
+ it('normalizes palette shorthand, replaces arrays and clears rules with null', () => {
+ expect(
+ mergeThemeConfig(
+ { colors: { axis: 'red', categorical: ['red', 'blue'] } },
+ { colors: ['green'] },
+ )?.colors,
+ ).toEqual({ axis: 'red', categorical: ['green'] });
+ expect(resolveTheme(mergeThemeConfig(broadsheet, { rule: null })).rule).toBeNull();
+ expect(resolveTheme(mergeThemeConfig(broadsheet, { rule: { width: 80 } })).rule).toMatchObject({
+ width: 80,
+ thickness: 3,
+ color: '#e3120b',
+ });
+ });
+ it('gives authored chrome size priority over general font sizes in every layer', () => {
+ const theme = resolveTheme(
+ mergeThemeConfig(
+ { chrome: { title: { fontSize: 33 }, source: { fontSize: 16 } } },
+ { fonts: { sizes: { title: 20, small: 10 } } },
+ ),
+ );
+ expect(theme.chrome.title.fontSize).toBe(33);
+ expect(theme.chrome.source.fontSize).toBe(16);
+ expect(theme.chrome.footer.fontSize).toBe(10);
+ });
+ it('uses D12 typography and D19 ink without changing default background or axis', () => {
+ const theme = resolveTheme();
+ expect(theme.fonts.sizes).toMatchObject({ small: 12, axisTick: 12 });
+ expect(theme.spacing).toMatchObject({ chromeToChart: 16, chartToFooter: 12 });
+ expect(theme.chrome.byline.fontSize).toBe(12);
+ expect(theme.colors).toMatchObject({
+ background: 'transparent',
+ axis: '#71717a',
+ annotationRule: '#888888',
+ annotationRangeFill: '#f0c040',
+ });
+ expect(adaptTheme(theme).colors.annotationRule).toBe('#9ca3af');
+ expect(clarity.fonts).toBeUndefined();
+ expect(clarity.spacing).toBeUndefined();
+ });
+ it('derives marker and plate colors from the actual surface and preserves explicit colors', () => {
+ for (const theme of [
+ resolveTheme({ colors: { background: '#faf8f5' } }),
+ resolveTheme(terminal),
+ adaptTheme(resolveTheme()),
+ ]) {
+ expect(theme.colors.annotationMarkerFill).toBe(theme.colors.neutral.surface);
+ expect(theme.colors.annotationPlate).toBe(theme.colors.neutral.surface);
+ }
+ const explicit = adaptTheme(
+ resolveTheme({
+ colors: {
+ annotationMarkerFill: '#ffffff',
+ annotationPlate: { light: '#ffffff', dark: '#eeeeee' },
+ },
+ }),
+ );
+ expect(explicit.colors.annotationMarkerFill).toBe('#ffffff');
+ expect(explicit.colors.annotationPlate).toBe('#eeeeee');
+ });
+});
diff --git a/packages/core/src/theme/dark-mode.ts b/packages/core/src/theme/dark-mode.ts
index f06b6f09..583d6b9b 100644
--- a/packages/core/src/theme/dark-mode.ts
+++ b/packages/core/src/theme/dark-mode.ts
@@ -164,6 +164,23 @@ export function adaptTheme(theme: ResolvedTheme): ResolvedTheme {
...CATEGORICAL_FILL_PALETTE_DARK,
]);
+ const neutral = deriveNeutralRamp(darkText, darkBg, true);
+ const authoredColors =
+ theme._authored?.colors && !Array.isArray(theme._authored.colors) ? theme._authored.colors : {};
+ const authoredColor = (
+ key:
+ | 'annotationLabel'
+ | 'annotationMuted'
+ | 'annotationLeader'
+ | 'annotationRule'
+ | 'annotationRangeFill'
+ | 'gridlineZero'
+ | 'mapInteriorBorder'
+ | 'mapOutlineBorder',
+ fallback: string,
+ ): string =>
+ pairs?.[`colors.${key}`]?.dark ??
+ (authoredColors[key] !== undefined ? (theme.colors[key] ?? fallback) : fallback);
return {
...theme,
isDark: true,
@@ -172,6 +189,7 @@ export function adaptTheme(theme: ResolvedTheme): ResolvedTheme {
background: darkBg,
text: darkText,
gridline: darkGridline,
+ gridlineZero: authoredColor('gridlineZero', 'rgba(255,255,255,0.24)'),
axis: darkAxis,
annotationFill: pairs?.['colors.annotationFill']
? pairs['colors.annotationFill'].dark
@@ -179,27 +197,25 @@ export function adaptTheme(theme: ResolvedTheme): ResolvedTheme {
annotationText: pairs?.['colors.annotationText']
? pairs['colors.annotationText'].dark
: overridden(theme.colors.annotationText, light.annotationText, darkMuted),
- annotationLabel:
- pairs?.['colors.annotationLabel']?.dark ??
- overridden(theme.colors.annotationLabel, light.annotationLabel, '#d1d5db'),
- annotationMuted:
- pairs?.['colors.annotationMuted']?.dark ??
- overridden(theme.colors.annotationMuted, light.annotationMuted, '#9ca3af'),
- annotationLeader:
- pairs?.['colors.annotationLeader']?.dark ??
- overridden(theme.colors.annotationLeader, light.annotationLeader, '#9aa0a6'),
- annotationRule:
- pairs?.['colors.annotationRule']?.dark ??
- overridden(theme.colors.annotationRule, light.annotationRule, '#666666'),
- annotationRangeFill:
- pairs?.['colors.annotationRangeFill']?.dark ??
- overridden(theme.colors.annotationRangeFill, light.annotationRangeFill, '#000000'),
+ annotationLabel: authoredColor('annotationLabel', '#d1d5db'),
+ annotationMuted: authoredColor('annotationMuted', '#9ca3af'),
+ annotationLeader: authoredColor('annotationLeader', '#9aa0a6'),
+ annotationRule: authoredColor('annotationRule', '#9ca3af'),
+ annotationRangeFill: authoredColor('annotationRangeFill', '#f0c040'),
annotationMarkerFill:
pairs?.['colors.annotationMarkerFill']?.dark ??
- overridden(theme.colors.annotationMarkerFill, light.annotationMarkerFill, '#0a0a0a'),
+ (authoredColors.annotationMarkerFill !== undefined
+ ? theme.colors.annotationMarkerFill
+ : neutral.surface),
annotationPlate:
pairs?.['colors.annotationPlate']?.dark ??
- overridden(theme.colors.annotationPlate, light.annotationPlate, '#0a0a0a'),
+ (authoredColors.annotationPlate !== undefined
+ ? theme.colors.annotationPlate
+ : neutral.surface),
+ mapNoDataFill: pairs?.['colors.mapNoDataFill']?.dark ?? theme.colors.mapNoDataFill,
+ mapNoDataHatch: pairs?.['colors.mapNoDataHatch']?.dark ?? theme.colors.mapNoDataHatch,
+ mapInteriorBorder: authoredColor('mapInteriorBorder', 'rgba(255,255,255,0.18)'),
+ mapOutlineBorder: authoredColor('mapOutlineBorder', 'rgba(255,255,255,0.38)'),
categorical,
categoricalFill,
hairline: darkHairline,
@@ -209,7 +225,7 @@ export function adaptTheme(theme: ResolvedTheme): ResolvedTheme {
negative: pairs?.['colors.negative']
? pairs['colors.negative'].dark
: overridden(theme.colors.negative, light.negative, '#f87171'),
- neutral: deriveNeutralRamp(darkText, darkBg, true),
+ neutral,
},
chrome: {
// Eyebrow keeps its accent tint (cyan in both modes) unless the user
@@ -253,11 +269,16 @@ export function adaptTheme(theme: ResolvedTheme): ResolvedTheme {
},
},
// The editorial rule is a brand device: it keeps its authored color unless
- // the theme gave an explicit dark half of the pair.
+ // the theme gave an explicit dark half of the pair. A partial rule with no
+ // color derives from text ink, so it follows the dark text.
rule:
theme.rule && pairs?.['rule.color']
? { ...theme.rule, color: pairs['rule.color'].dark }
- : theme.rule,
+ : theme.rule &&
+ theme._authored?.rule?.color === undefined &&
+ theme.rule.color === theme.colors.text
+ ? { ...theme.rule, color: darkText }
+ : theme.rule,
};
}
diff --git a/packages/core/src/theme/defaults.ts b/packages/core/src/theme/defaults.ts
index 8810ff17..0b8f301c 100644
--- a/packages/core/src/theme/defaults.ts
+++ b/packages/core/src/theme/defaults.ts
@@ -18,6 +18,18 @@ import {
import { X_AXIS_BAND_HEIGHT } from '../responsive/metrics';
import type { ThemeBase } from '../types/theme';
+const FONT_SIZES = {
+ title: 26,
+ subtitle: 14,
+ body: 13,
+ small: 12,
+ axisTick: 12,
+ metricLabel: 11,
+ metricValue: 22,
+ endpointLabel: 13,
+ endpointValue: 12,
+};
+
/**
* The default theme. All fields are required and fully specified.
*/
@@ -29,7 +41,10 @@ export const DEFAULT_THEME: ThemeBase = {
diverging: DIVERGING_PALETTES,
background: 'transparent',
text: '#09090b',
- gridline: 'rgba(0,0,0,0.08)',
+ gridline: 'rgba(0,0,0,0.05)',
+ gridlineZero: 'rgba(0,0,0,0.22)',
+ mapInteriorBorder: 'rgba(0,0,0,0.14)',
+ mapOutlineBorder: 'rgba(0,0,0,0.40)',
// Structural hairline: axis lines, separators. Deliberately heavier than
// the gridline and lighter than any ink, so structure reads as structure.
hairline: 'rgba(0,0,0,0.14)',
@@ -42,8 +57,8 @@ export const DEFAULT_THEME: ThemeBase = {
annotationLabel: '#333333',
annotationMuted: '#6b7280',
annotationLeader: '#8f8f8f',
- annotationRule: '#666666',
- annotationRangeFill: '#000000',
+ annotationRule: '#888888',
+ annotationRangeFill: '#f0c040',
annotationMarkerFill: '#ffffff',
annotationPlate: '#ffffff',
positive: '#15803d',
@@ -53,15 +68,7 @@ export const DEFAULT_THEME: ThemeBase = {
family:
'"Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
mono: '"JetBrains Mono", "Fira Code", "Cascadia Code", monospace',
- sizes: {
- title: 26,
- subtitle: 14,
- body: 13,
- small: 11,
- axisTick: 11,
- metricLabel: 11,
- metricValue: 22,
- },
+ sizes: FONT_SIZES,
// One ladder everywhere: 400 regular / 500 medium (eyebrow, labels, UI,
// values) / 600 semibold (title, series names, annotation lede) / 700 bold.
weights: {
@@ -74,8 +81,8 @@ export const DEFAULT_THEME: ThemeBase = {
spacing: {
padding: 20,
chromeGap: 6,
- chromeToChart: 8,
- chartToFooter: 8,
+ chromeToChart: 16,
+ chartToFooter: 12,
axisMargin: 6,
xAxisHeight: X_AXIS_BAND_HEIGHT,
// Gap (px) between the x-axis line and the TOP of the tick labels. The
@@ -84,6 +91,9 @@ export const DEFAULT_THEME: ThemeBase = {
// dominant-baseline:hanging, which WebKit positions from different metrics),
// so the gap holds regardless of font size.
xAxisLabelPadding: 4,
+ endpointEntryGap: 6,
+ endpointValueGap: 4,
+ endpointColumnGap: 18,
},
/** Containers and tooltips. Marks carry their own 2px radius constant. */
borderRadius: 8,
@@ -95,31 +105,31 @@ export const DEFAULT_THEME: ThemeBase = {
lineHeight: 1.4,
},
title: {
- fontSize: 26,
+ fontSize: FONT_SIZES.title,
fontWeight: 600,
color: '#09090b',
lineHeight: 1.15,
},
subtitle: {
- fontSize: 14,
+ fontSize: FONT_SIZES.subtitle,
fontWeight: 400,
color: '#71717a',
lineHeight: 1.45,
},
source: {
- fontSize: 11,
+ fontSize: FONT_SIZES.small,
fontWeight: 400,
color: '#71717a',
lineHeight: 1.4,
},
byline: {
- fontSize: 11,
+ fontSize: FONT_SIZES.small,
fontWeight: 400,
color: '#71717a',
lineHeight: 1.4,
},
footer: {
- fontSize: 11,
+ fontSize: FONT_SIZES.small,
fontWeight: 400,
color: '#71717a',
lineHeight: 1.4,
diff --git a/packages/core/src/theme/index.ts b/packages/core/src/theme/index.ts
index d9256d44..489df80a 100644
--- a/packages/core/src/theme/index.ts
+++ b/packages/core/src/theme/index.ts
@@ -5,5 +5,5 @@
export { adaptColorForDarkMode, adaptForLightLineStroke, adaptTheme } from './dark-mode';
export { DEFAULT_THEME } from './defaults';
export { mergeThemeConfig } from './merge';
-export { broadsheet, clarity, editorial, essay, legacyEditorial, terminal, wire } from './presets';
+export { broadsheet, clarity, essay, legacyEditorial, terminal, wire } from './presets';
export { resolveTheme } from './resolve';
diff --git a/packages/core/src/theme/merge.ts b/packages/core/src/theme/merge.ts
index f6f8057c..8dba345d 100644
--- a/packages/core/src/theme/merge.ts
+++ b/packages/core/src/theme/merge.ts
@@ -1,9 +1,30 @@
import type { ThemeConfig } from '../types/spec';
-/** Merge authored theme layers with v8 shallow precedence (later layers win). */
+function isRecord(value: unknown): value is Record {
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
+}
+
+/** Merge configuration objects; color pairs, arrays and null are atomic leaves. */
+export function mergeThemeObjects(base: T, override: unknown): T {
+ if (!isRecord(override) || 'light' in override || 'dark' in override) return override as T;
+ const result: Record = isRecord(base) ? { ...base } : {};
+ for (const [key, value] of Object.entries(override)) {
+ if (value !== undefined) result[key] = mergeThemeObjects(result[key], value);
+ }
+ return result as T;
+}
+
+function normalize(config: ThemeConfig): ThemeConfig {
+ return Array.isArray(config.colors)
+ ? { ...config, colors: { categorical: config.colors } }
+ : config;
+}
+
+/** Deep-merge authored theme layers. Later layers win at each authored leaf. */
export function mergeThemeConfig(
base: ThemeConfig | undefined,
override: ThemeConfig | undefined,
): ThemeConfig | undefined {
- return override ? { ...base, ...override } : base;
+ if (!override) return base;
+ return mergeThemeObjects(base ? normalize(base) : {}, normalize(override));
}
diff --git a/packages/core/src/theme/presets.ts b/packages/core/src/theme/presets.ts
index 708c4160..626890a6 100644
--- a/packages/core/src/theme/presets.ts
+++ b/packages/core/src/theme/presets.ts
@@ -9,9 +9,6 @@
import { CATEGORICAL_FILL_PALETTE_DARK, CATEGORICAL_PALETTE_DARK } from '../colors/palettes';
import type { ThemeConfig } from '../types/spec';
-/** Editorial preset: the current default look. Exported for explicit selection. */
-export const editorial: ThemeConfig = {};
-
/** Essay preset: serif display titles, warmer background, generous spacing. */
export const essay: ThemeConfig = {
colors: {
@@ -103,7 +100,7 @@ export const broadsheet: ThemeConfig = {
},
chrome: {
eyebrow: { color: { light: '#e3120b', dark: '#f4463f' }, fontSize: 11, fontWeight: 700 },
- title: { fontSize: 24, fontWeight: 700, lineHeight: 1.18 },
+ title: { fontWeight: 700, lineHeight: 1.18 },
subtitle: { fontWeight: 400, lineHeight: 1.45 },
},
/** The masthead device: a short red bar above the eyebrow. */
@@ -149,26 +146,22 @@ export const terminal: ThemeConfig = {
},
chrome: {
eyebrow: { color: { light: '#22d3ee', dark: '#22d3ee' }, fontSize: 10, fontWeight: 500 },
- title: { fontSize: 15, fontWeight: 600, lineHeight: 1.3 },
+ title: { fontWeight: 600, lineHeight: 1.3 },
subtitle: { fontWeight: 400, lineHeight: 1.4 },
},
borderRadius: 4,
seriesStrategy: 'accent-neutral',
};
-/** Next-generation editorial composition; opt in during v8. */
+/** @deprecated v9 adopts clarity typography by default. Set its remaining colors explicitly; removed in v10. */
export const clarity: ThemeConfig = {
colors: {
background: { light: '#ffffff', dark: '#09090b' },
axis: { light: '#52525b', dark: '#a1a1aa' },
},
- fonts: { sizes: { title: 26, subtitle: 14, body: 13, small: 12, axisTick: 12 } },
chrome: {
eyebrow: { color: { light: '#0f766e', dark: '#5eead4' } },
- source: { fontSize: 12 },
- footer: { fontSize: 12 },
},
- spacing: { chromeToChart: 16, chartToFooter: 12 },
};
/** Documented v8 typography. This does not freeze the layout engine. */
diff --git a/packages/core/src/theme/resolve.ts b/packages/core/src/theme/resolve.ts
index cb9c1e54..470cd439 100644
--- a/packages/core/src/theme/resolve.ts
+++ b/packages/core/src/theme/resolve.ts
@@ -18,35 +18,7 @@ import type { ChromeDefaults, ResolvedTheme, ThemeBase, TokenValue } from '../ty
import { DEFAULT_THEME } from './defaults';
-/**
- * Deep merge source into target, creating a new object.
- * Only merges plain objects; arrays and primitives replace directly.
- */
-// biome-ignore lint/suspicious/noExplicitAny: recursive object merge requires dynamic typing
-function deepMerge(target: any, source: any): any {
- const result = { ...target };
-
- for (const key of Object.keys(source)) {
- const sourceVal = source[key];
- const targetVal = target[key];
-
- if (
- sourceVal !== undefined &&
- sourceVal !== null &&
- typeof sourceVal === 'object' &&
- !Array.isArray(sourceVal) &&
- typeof targetVal === 'object' &&
- targetVal !== null &&
- !Array.isArray(targetVal)
- ) {
- result[key] = deepMerge(targetVal, sourceVal);
- } else if (sourceVal !== undefined) {
- result[key] = sourceVal;
- }
- }
-
- return result;
-}
+import { mergeThemeObjects } from './merge';
/** Extract the light-mode string from a TokenValue. */
function resolveTokenLight(v: TokenValue): string {
@@ -57,7 +29,7 @@ function resolveTokenLight(v: TokenValue): string {
* Convert a user-facing ThemeConfig (partial) into the full ThemeBase shape
* that deepMerge can work with.
*/
-function themeConfigToPartial(config: ThemeConfig): Partial {
+function themeConfigToPartial(config: ThemeConfig, base: ThemeBase): Partial {
const partial: Partial = {};
if (config.colors) {
@@ -93,6 +65,16 @@ function themeConfigToPartial(config: ThemeConfig): Partial {
colors.annotationMarkerFill = resolveTokenLight(config.colors.annotationMarkerFill);
if (config.colors.annotationPlate)
colors.annotationPlate = resolveTokenLight(config.colors.annotationPlate);
+ for (const key of [
+ 'gridlineZero',
+ 'mapNoDataFill',
+ 'mapNoDataHatch',
+ 'mapInteriorBorder',
+ 'mapOutlineBorder',
+ ] as const) {
+ const value = config.colors[key];
+ if (value !== undefined) colors[key] = resolveTokenLight(value);
+ }
if (config.colors.positive) colors.positive = resolveTokenLight(config.colors.positive);
if (config.colors.negative) colors.negative = resolveTokenLight(config.colors.negative);
}
@@ -126,6 +108,9 @@ function themeConfigToPartial(config: ThemeConfig): Partial {
if (config.spacing.xAxisHeight !== undefined) spacing.xAxisHeight = config.spacing.xAxisHeight;
if (config.spacing.xAxisLabelPadding !== undefined)
spacing.xAxisLabelPadding = config.spacing.xAxisLabelPadding;
+ for (const key of ['endpointEntryGap', 'endpointValueGap', 'endpointColumnGap'] as const) {
+ if (config.spacing[key] !== undefined) spacing[key] = config.spacing[key];
+ }
partial.spacing = spacing as ThemeBase['spacing'];
}
@@ -137,9 +122,9 @@ function themeConfigToPartial(config: ThemeConfig): Partial {
const chrome: Record> = {};
for (const [element, value] of Object.entries(config.chrome)) {
if (value === undefined) continue;
- if (typeof value === 'string') {
- chrome[element] = { color: value };
- } else {
+ if (typeof value === 'string')
+ throw new Error('theme.chrome string colors were removed in v9; use { color }.');
+ {
const override: Partial = {};
const obj = value as ChromeThemeOverride;
if (obj.color !== undefined) override.color = resolveTokenLight(obj.color);
@@ -154,12 +139,19 @@ function themeConfigToPartial(config: ThemeConfig): Partial {
if (config.rule) {
partial.rule = {
- color: resolveTokenLight(config.rule.color),
- width: config.rule.width,
- thickness: config.rule.thickness,
+ color: resolveTokenLight(
+ config.rule.color ??
+ base.rule?.color ??
+ (config.colors && !Array.isArray(config.colors) ? config.colors.text : undefined) ??
+ DEFAULT_THEME.colors.text,
+ ),
+ width: config.rule.width ?? base.rule?.width ?? 40,
+ thickness: config.rule.thickness ?? base.rule?.thickness ?? 3,
};
}
+ if (config.rule === null) partial.rule = null;
+
if (config.seriesStrategy) {
partial.seriesStrategy = config.seriesStrategy;
}
@@ -180,6 +172,7 @@ function collectTokenPairs(
'background',
'text',
'gridline',
+ 'gridlineZero',
'hairline',
'axis',
'annotationFill',
@@ -191,6 +184,10 @@ function collectTokenPairs(
'annotationRangeFill',
'annotationMarkerFill',
'annotationPlate',
+ 'mapNoDataFill',
+ 'mapNoDataHatch',
+ 'mapInteriorBorder',
+ 'mapOutlineBorder',
'positive',
'negative',
] as const;
@@ -322,38 +319,33 @@ export function resolveTheme(
base: ThemeBase = DEFAULT_THEME,
): ResolvedTheme {
let merged: ThemeBase = userTheme
- ? deepMerge(base, themeConfigToPartial(userTheme))
+ ? mergeThemeObjects(base, themeConfigToPartial(userTheme, base))
: { ...base };
- // Propagate fonts.sizes overrides onto the chrome element fontSizes.
- // The chrome defaults hard-code fontSize independently from fonts.sizes,
- // so we sync them here after merging so a single sizes override applies everywhere.
- if (userTheme?.fonts?.sizes) {
- const s = userTheme.fonts.sizes;
- merged = {
- ...merged,
- chrome: {
- ...merged.chrome,
- ...(s.title !== undefined && {
- title: { ...merged.chrome.title, fontSize: s.title },
- }),
- ...(s.subtitle !== undefined && {
- subtitle: { ...merged.chrome.subtitle, fontSize: s.subtitle },
- }),
- ...(s.small !== undefined && {
- source: { ...merged.chrome.source, fontSize: s.small },
- byline: { ...merged.chrome.byline, fontSize: s.small },
- footer: { ...merged.chrome.footer, fontSize: s.small },
- }),
- },
- };
+ // General font sizes supply defaults; authored chrome leaves are more specific.
+ for (const [element, size] of Object.entries({
+ title: 'title',
+ subtitle: 'subtitle',
+ source: 'small',
+ byline: 'small',
+ footer: 'small',
+ }) as [keyof ThemeBase['chrome'], 'title' | 'subtitle' | 'small'][]) {
+ if (userTheme?.chrome?.[element]?.fontSize === undefined) {
+ merged = {
+ ...merged,
+ chrome: {
+ ...merged.chrome,
+ [element]: { ...merged.chrome[element], fontSize: merged.fonts.sizes[size] },
+ },
+ };
+ }
}
// Auto-adapt chrome for dark backgrounds
const dark = isDarkBackground(merged.colors.background);
if (dark) {
- merged = adaptChromeForDarkBg(merged, merged.colors.text);
- // These defaults historically followed a dark canvas even without darkMode.
+ merged = adaptChromeForDarkBg({ ...merged, colors: { ...merged.colors } }, merged.colors.text);
+ // Default ink follows a dark authored canvas even without a runtime mode flag.
const authoredColors =
userTheme?.colors && !Array.isArray(userTheme.colors) ? userTheme.colors : {};
for (const key of [
@@ -364,6 +356,9 @@ export function resolveTheme(
'annotationRangeFill',
'annotationMarkerFill',
'annotationPlate',
+ 'gridlineZero',
+ 'mapInteriorBorder',
+ 'mapOutlineBorder',
] as const) {
if (authoredColors[key] === undefined)
merged.colors[key] = cssTokenDefault(
@@ -373,11 +368,23 @@ export function resolveTheme(
}
}
+ const neutral = deriveNeutralRamp(merged.colors.text, merged.colors.background, dark);
+ const authoredColors =
+ userTheme?.colors && !Array.isArray(userTheme.colors) ? userTheme.colors : {};
return {
...merged,
+ _authored: userTheme,
colors: {
...merged.colors,
- neutral: deriveNeutralRamp(merged.colors.text, merged.colors.background, dark),
+ neutral,
+ annotationMarkerFill:
+ authoredColors.annotationMarkerFill === undefined
+ ? neutral.surface
+ : merged.colors.annotationMarkerFill,
+ annotationPlate:
+ authoredColors.annotationPlate === undefined
+ ? neutral.surface
+ : merged.colors.annotationPlate,
},
isDark: dark,
_tokenPairs: userTheme ? collectTokenPairs(userTheme) : undefined,
diff --git a/packages/core/src/types/design.ts b/packages/core/src/types/design.ts
index 40bf0e4e..e8a6ab9a 100644
--- a/packages/core/src/types/design.ts
+++ b/packages/core/src/types/design.ts
@@ -1,3 +1,4 @@
+import type { EditOp } from './events';
import type { DataRow, VizSpec } from './spec';
/** Supplied semantics; the library never invents a measure or uncertainty. */
@@ -27,12 +28,17 @@ export interface DesignDiagnostic {
fix?: SpecPatch;
}
-/** Atomic JSON-pointer edits guarded by the complete source document. */
+/**
+ * Atomic edits guarded by the complete source document. `changes` uses the same
+ * `EditOp` semantics as `applyEditTransaction`: `set` replaces a value whose
+ * parent exists, `add` also creates missing object containers, and `remove`
+ * deletes a value.
+ */
export interface SpecPatch {
label: string;
/** Canonical source snapshot prevents stale fixes overwriting subsequent edits. */
precondition: string;
- changes: Array<{ path: string; value: unknown }>;
+ changes: EditOp[];
}
export type DesignQuestion =
diff --git a/packages/core/src/types/events.ts b/packages/core/src/types/events.ts
index 372f2c53..ca8daa2e 100644
--- a/packages/core/src/types/events.ts
+++ b/packages/core/src/types/events.ts
@@ -9,21 +9,14 @@
* mark elements and constructs these typed events from the raw browser events.
*/
-import type {
- Annotation,
- AnnotationOffset,
- DataRow,
- RangeAnnotation,
- RefLineAnnotation,
- TextAnnotation,
-} from './spec';
+import type { Annotation, DataRow } from './spec';
/** A source-addressed durable edit. Paths use escaped RFC 6901 JSON Pointers. */
export interface EditOp {
op: 'set' | 'remove' | 'add';
path: string;
value?: unknown;
- /** Preconditions on the nearest authored annotation ancestor. */
+ /** Preconditions on the nearest authored annotation ancestor. `text` matches a refline/range `label`. */
expect?: { id?: string; type?: string; text?: string };
}
@@ -55,7 +48,7 @@ export type ElementRef =
type: 'annotation';
index: number;
id?: string;
- /** RFC 6901 JSON Pointer to the edited property in the spec. */
+ /** Root-relative RFC 6901 JSON Pointer to the authored object this ref identifies (e.g. `/layer/0/annotations/2`, `/chrome/title`). Always set on refs the chart emits or returns; legend refs point at `/legend`. Edited properties are addressed by `EditOp.path`. */
path?: string;
/** Rendered facet instance; source identity remains index/id/path. */
facetInstance?: string;
@@ -63,25 +56,19 @@ export type ElementRef =
| {
type: 'chrome';
key: ChromeKey;
- /** RFC 6901 JSON Pointer to the edited property in the spec. */
- path?: string;
- }
- | {
- type: 'series-label';
- series: string;
- /** RFC 6901 JSON Pointer to the edited property in the spec. */
+ /** Root-relative RFC 6901 JSON Pointer to the authored object this ref identifies (e.g. `/layer/0/annotations/2`, `/chrome/title`). Always set on refs the chart emits or returns; legend refs point at `/legend`. Edited properties are addressed by `EditOp.path`. */
path?: string;
}
| {
type: 'legend';
- /** RFC 6901 JSON Pointer to the edited property in the spec. */
+ /** Root-relative RFC 6901 JSON Pointer to the authored object this ref identifies (e.g. `/layer/0/annotations/2`, `/chrome/title`). Always set on refs the chart emits or returns; legend refs point at `/legend`. Edited properties are addressed by `EditOp.path`. */
path?: string;
}
| {
type: 'legend-entry';
series: string;
index: number;
- /** RFC 6901 JSON Pointer to the edited property in the spec. */
+ /** Root-relative RFC 6901 JSON Pointer to the authored object this ref identifies (e.g. `/layer/0/annotations/2`, `/chrome/title`). Always set on refs the chart emits or returns; legend refs point at `/legend`. Edited properties are addressed by `EditOp.path`. */
path?: string;
};
@@ -102,11 +89,6 @@ export const elementRef = {
key,
...(path !== undefined ? { path } : {}),
}),
- seriesLabel: (series: string, path?: string): ElementRef => ({
- type: 'series-label',
- series,
- ...(path !== undefined ? { path } : {}),
- }),
legend: (path?: string): ElementRef => ({
type: 'legend',
...(path !== undefined ? { path } : {}),
@@ -119,73 +101,6 @@ export const elementRef = {
}),
} as const;
-// ---------------------------------------------------------------------------
-// Element edit events
-// ---------------------------------------------------------------------------
-
-/**
- * Discriminated union of all element edit events.
- * Fired by the `onEdit` callback when any editable chart element is modified.
- * Covers repositioning (drag), deletion (Delete key), and text editing (double-click).
- * Selection events (onSelect/onDeselect) are separate callbacks, not part of this union.
- *
- * For the `annotation` variant:
- * - `annotation` is the ORIGINAL spec annotation (may have a stale or missing offset)
- * - `offset` is the NEW accumulated position after the drag completes
- * - To persist a drag edit back into your spec: `{ ...edit.annotation, offset: edit.offset }`
- */
-export type ElementEdit =
- | {
- type: 'annotation';
- element: ElementRef;
- annotation: TextAnnotation;
- offset: AnnotationOffset;
- /** Resolved first-drag placement. Persist together with offset when supplied. */
- anchor?: TextAnnotation['anchor'];
- /** A moved annotation stays inline. */
- responsive?: false;
- }
- | {
- type: 'annotation-connector';
- element: ElementRef;
- annotation: TextAnnotation;
- endpoint: 'from' | 'to';
- offset: AnnotationOffset;
- }
- | {
- type: 'range-label';
- element: ElementRef;
- annotation: RangeAnnotation;
- labelOffset: AnnotationOffset;
- }
- | {
- type: 'refline-label';
- element: ElementRef;
- annotation: RefLineAnnotation;
- labelOffset: AnnotationOffset;
- }
- | {
- type: 'annotation-anchor';
- element: ElementRef;
- annotation: TextAnnotation;
- x: string | number;
- y: string | number;
- }
- | { type: 'chrome'; key: ChromeKey; text: string; offset: AnnotationOffset }
- | { type: 'series-label'; series: string; offset: AnnotationOffset }
- | { type: 'legend'; offset: AnnotationOffset }
- /**
- * Transient legend visibility toggle. Reports the same runtime show/hide
- * a reader triggers; not persisted by openchart. To author default-hidden
- * series, set `spec.hiddenSeries` directly. Hosts building spec
- * persistence should generally not write legend-toggle edits back into
- * the authored spec. `hidden: true` means the series was just hidden.
- */
- | { type: 'legend-toggle'; series: string; hidden: boolean }
- | { type: 'add'; annotation: TextAnnotation }
- | { type: 'delete'; element: ElementRef }
- | { type: 'text-edit'; element: ElementRef; oldText: string; newText: string };
-
// ---------------------------------------------------------------------------
// Mark events
// ---------------------------------------------------------------------------
@@ -218,14 +133,6 @@ export interface MarkEvent {
* or ChartProps (React component) to receive interaction callbacks.
*/
export interface ChartEventHandlers {
- /**
- * Explicitly enable or disable edit interactions (drag, select, keyboard
- * delete/edit). When true, the interaction layer is wired even without
- * onEdit. When false, all edit interactions are suppressed even when
- * onEdit is provided. When omitted, each interaction block uses its
- * existing callback-presence gate.
- */
- editable?: boolean;
/** Called when a data mark is clicked. */
onMarkClick?: (event: MarkEvent) => void;
/** Called when the mouse enters a data mark. */
@@ -242,20 +149,14 @@ export interface ChartEventHandlers {
onHighlightChange?: (values: string[]) => void;
/** Called when an annotation element is clicked. */
onAnnotationClick?: (annotation: Annotation, event: MouseEvent, element: ElementRef) => void;
- /** @deprecated Use onEditTransaction to persist complete edits. */
- onAnnotationEdit?: (annotation: TextAnnotation, updatedOffset: AnnotationOffset) => void;
/** Unified edit callback. Fires for any spec-modifying edit (repositioning, deletion, text editing). */
- onEdit?: (edit: ElementEdit) => void;
+ onEdit?: (transaction: EditTransaction) => void;
/** Optional source revision, stamped on emitted transactions. */
revision?: string | number;
- /** Durable authored edits, applied atomically with applyEditTransaction. */
- onEditTransaction?: (transaction: EditTransaction) => void;
/** Fired when an element is selected via click or programmatic select(). */
onSelect?: (element: ElementRef) => void;
/** Fired when the current element is deselected (click empty, Escape, or new selection replaces old). */
onDeselect?: (element: ElementRef) => void;
- /** @deprecated Use onEditTransaction with a text set operation. */
- onTextEdit?: (element: ElementRef, oldText: string, newText: string) => void;
/**
* Fired when a "you draw it" (`youDrawIt`) drawing is revealed (button click
* or programmatic reveal), with the reader's guess in data coordinates,
diff --git a/packages/core/src/types/geo-color-key.ts b/packages/core/src/types/geo-color-key.ts
new file mode 100644
index 00000000..29ce0a95
--- /dev/null
+++ b/packages/core/src/types/geo-color-key.ts
@@ -0,0 +1,33 @@
+import type { Rect } from './layout';
+export interface GeoColorKeyLayout {
+ kind: 'binned' | 'gradient' | 'categorical';
+ field: string;
+ format?: string;
+ activeValue: boolean;
+ distribution: boolean;
+ /** Exact existing classes. Counts are finite joined observations, never area. */
+ classes: Array<{
+ index: number;
+ color: string;
+ lower?: number;
+ upper?: number;
+ count: number;
+ x: number;
+ width: number;
+ y?: number;
+ height?: number;
+ }>;
+ categories: Array<{ value: string; color: string; count: number; bounds: Rect }>;
+ missing: number;
+ bar?: Rect;
+ domain?: [number, number];
+ clampDomain?: [number, number];
+ /** Measured exact-value lines keyed by tagged raw value; A prefix is replaced by the slot. */
+ activeLabels?: Record;
+ activeRows: Rect[];
+ distributionBounds?: Rect;
+}
+export interface GeoColorKeyDescriptor {
+ features?: GeoColorKeyLayout;
+ points?: GeoColorKeyLayout;
+}
diff --git a/packages/core/src/types/geo-labels.ts b/packages/core/src/types/geo-labels.ts
new file mode 100644
index 00000000..729d663a
--- /dev/null
+++ b/packages/core/src/types/geo-labels.ts
@@ -0,0 +1,106 @@
+/** Portable, measured map-label payload. All coordinates are map-local CSS pixels. */
+import type { DesignDiagnostic } from './design';
+import type { Point, Rect, TextStyle } from './layout';
+import type { GeoAnchor } from './spec';
+
+export interface GeoTextBlock {
+ width: number;
+ height: number;
+ /** Baselines relative to the block's top-left corner. */
+ lines: Array<{ text: string; x: number; y: number; style: TextStyle }>;
+}
+export interface GeoLabelComponent {
+ index: number;
+ exterior: Point[];
+ holes: Point[][];
+ /** Original SVG nonzero-fill orientation, preserved through normalized storage. */
+ winding?: number;
+ holeWindings?: number[];
+}
+export interface GeoLabelGeometry {
+ featureId: string;
+ bounds: Rect;
+ canonicalAnchor?: Point;
+ components?: GeoLabelComponent[];
+ vertexCount: number;
+ mode: 'retained' | 'fallback' | 'unresolved';
+ searchLimited?: boolean;
+ captureLimited?: boolean;
+}
+export interface GeoLabelCandidate {
+ /** Stable tagged identity, distinct from authored callout and entity IDs. */
+ id: string;
+ kind: 'text' | 'callout';
+ anchor: GeoAnchor;
+ text: string;
+ subtitle?: string;
+ priority: number;
+ block: GeoTextBlock;
+ /** Projected point/coordinate anchor; feature anchors use geometryId. */
+ resolvedAnchor?: Point;
+ /** Constant-screen radius of an anchored quantitative symbol. */
+ anchorRadius?: number;
+ geometryId?: string;
+ minZoom?: number;
+ maxZoom?: number;
+ offset?: { dx: number; dy: number };
+ /** Stable index into the premeasured authored callout note slots. */
+ noteSlot?: number;
+}
+export interface GeoCalloutNoteSlot {
+ id: string;
+ number: number;
+ bounds: Rect;
+ block: GeoTextBlock;
+}
+export type GeoLabelVisibility =
+ | 'visible'
+ | 'collision'
+ | 'zoom-range'
+ | 'offscreen'
+ | 'unresolved'
+ | 'geometry-budget';
+export interface GeoLabelInventoryEntry {
+ id: string;
+ kind: 'text' | 'callout';
+ anchor: GeoAnchor;
+ text: string;
+ subtitle?: string;
+ visibility: GeoLabelVisibility;
+ presentation: 'label' | 'note' | 'none';
+ limitation?: 'candidate-limit' | 'vertex-limit' | 'search-limit';
+}
+export interface GeoPlacedLabel {
+ id: string;
+ anchor: Point;
+ bounds: Rect;
+ block: GeoTextBlock;
+}
+export interface GeoLabelOverlay {
+ diagnostics?: DesignDiagnostic[];
+ labels: GeoPlacedLabel[];
+ leaders: Array<{ id: string; from: Point; to: Point }>;
+ markers: Array<{ id: string; anchor: Point; number: number }>;
+ notes: Array<{ id: string; slot: GeoCalloutNoteSlot }>;
+ inventory: GeoLabelInventoryEntry[];
+}
+export interface GeoLabelsInput {
+ /** Complete inventory source, including candidates beyond the visual cap. */
+ candidates: GeoLabelCandidate[];
+ geometries: GeoLabelGeometry[];
+ viewport: Rect;
+ obstacles: Rect[];
+ /** Measured screen radius for fallback note markers. */
+ markerRadius?: number;
+ /** Base-map centers with constant-screen quantitative symbol radii. */
+ symbolObstacles?: Array<{ center: Point; radius: number }>;
+ noteSlots: GeoCalloutNoteSlot[];
+}
+export interface GeoLabelsBundle extends GeoLabelsInput {
+ initialOverlay: GeoLabelOverlay;
+}
+export interface GeoLabelAffine {
+ k: number;
+ tx: number;
+ ty: number;
+}
diff --git a/packages/core/src/types/geo-reader.ts b/packages/core/src/types/geo-reader.ts
new file mode 100644
index 00000000..5040e511
--- /dev/null
+++ b/packages/core/src/types/geo-reader.ts
@@ -0,0 +1,20 @@
+import type { DataRow } from './spec';
+
+export interface GeoEntityRef {
+ kind: 'feature' | 'point';
+ id: string | number;
+ canonicalId: string;
+}
+export interface GeoReaderObservation {
+ entity: GeoEntityRef;
+ observation: string;
+ persistent: boolean;
+ datum: DataRow | null;
+ previous?: DataRow | null;
+ state: 'visible' | 'filtered';
+}
+export interface GeoReaderSelection {
+ preview: GeoReaderObservation | null;
+ pinned: GeoReaderObservation[];
+ announcement: string;
+}
diff --git a/packages/core/src/types/geo-views.ts b/packages/core/src/types/geo-views.ts
new file mode 100644
index 00000000..7f76b8d2
--- /dev/null
+++ b/packages/core/src/types/geo-views.ts
@@ -0,0 +1,66 @@
+import type { GeoLabelsBundle, GeoTextBlock } from './geo-labels';
+import type {
+ GeoMapBorders,
+ GeoMapFeatureMark,
+ GeoMapFocusLayout,
+ GeoMapPointMark,
+ Rect,
+} from './layout';
+import type { GeoMapProjection } from './spec';
+
+/** Context geometry uses the primary view's immutable fitted projection. */
+export interface GeoDetailLayer {
+ id: string;
+ features: unknown;
+ objectKey?: string;
+ minZoom: number;
+ maxZoom?: number;
+ /** Assertion that decoded identity coordinates share the primary source plane. */
+ sourcePlane?: string;
+}
+export interface GeoInset {
+ id: string;
+ label: string;
+ /** Canonical topology feature IDs, independent of the geo.idField data-join field. */
+ featureIds: Array;
+ placement: 'bottom-left' | 'bottom-right';
+ width: number;
+ projection?: GeoMapProjection;
+ /** Defaults true; false explicitly duplicates primary feature coverage. */
+ removeFromMain?: boolean;
+ /** Explicit existing points.key values; omission includes no points. */
+ pointIds?: Array;
+}
+export interface GeoProjectionContext {
+ projection: GeoMapProjection;
+ space: 'geographic' | 'source';
+ sourcePlane?: string;
+ objectKey?: string;
+ fitObjectKey?: string;
+}
+export interface GeoDetailLayerLayout {
+ id: string;
+ minZoom: number;
+ maxZoom?: number;
+ /** Unstyled, context-only paths, already projected into this view. */
+ paths: string[];
+}
+/** Each view owns its camera; observation identity deliberately excludes viewId. */
+export interface GeoViewLayout {
+ viewId: string;
+ /** Map-local frame and drawing viewport, including the separately measured label. */
+ frame: Rect;
+ viewport: Rect;
+ labelBounds?: Rect;
+ label?: GeoTextBlock;
+ coverage?: string;
+ /** Everything below is viewport-local, in CSS pixels before camera transform. */
+ mapSize: { width: number; height: number };
+ features: GeoMapFeatureMark[];
+ borders: GeoMapBorders;
+ pointMarks: GeoMapPointMark[];
+ labels?: GeoLabelsBundle;
+ focus: GeoMapFocusLayout | null;
+ detailLayers: GeoDetailLayerLayout[];
+ projectionContext: GeoProjectionContext;
+}
diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts
index 83becb22..1265360b 100644
--- a/packages/core/src/types/index.ts
+++ b/packages/core/src/types/index.ts
@@ -32,11 +32,20 @@ export type {
ChromeKey,
EditOp,
EditTransaction,
- ElementEdit,
ElementRef,
MarkEvent,
} from './events';
export { elementRef } from './events';
+export type { GeoColorKeyDescriptor, GeoColorKeyLayout } from './geo-color-key';
+export type * from './geo-labels';
+export type * from './geo-reader';
+export type {
+ GeoDetailLayer,
+ GeoDetailLayerLayout,
+ GeoInset,
+ GeoProjectionContext,
+ GeoViewLayout,
+} from './geo-views';
// Layout types (engine output)
export type {
A11yMetadata,
@@ -132,6 +141,7 @@ export type {
YouDrawItSample,
YouDrawItYInvert,
} from './layout';
+export type * from './reader-slices';
// Spec types (user input)
export type {
A11yConfig,
@@ -186,6 +196,8 @@ export type {
FilterPredicate,
FilterTransform,
FoldTransform,
+ GeoAnchor,
+ GeoCallout,
GeoMapEncoding,
GeoMapFocus,
GeoMapGeo,
@@ -309,7 +321,6 @@ export type {
ResolvedTheme,
ResolvedThemeColors,
SeriesStrategy,
- Theme,
ThemeChromeDefaults,
ThemeColors,
ThemeFontSizes,
diff --git a/packages/core/src/types/layout.ts b/packages/core/src/types/layout.ts
index 1cc7d551..ad2e963f 100644
--- a/packages/core/src/types/layout.ts
+++ b/packages/core/src/types/layout.ts
@@ -9,6 +9,7 @@
* fully resolved with computed positions, colors, and dimensions.
*/
+import type { GeoLabelsBundle } from './geo-labels';
import type { GradientDef } from './spec';
import type { ResolvedTheme } from './theme';
@@ -128,6 +129,10 @@ export interface ResolvedChrome {
byline?: ResolvedChromeElement;
footer?: ResolvedChromeElement;
brand?: ResolvedChromeElement;
+ /** Measured default attribution mark; renderers use its exact size and position. */
+ watermark?: ResolvedChromeElement;
+ /** Resolved footer composition; independent of temporary interaction state. */
+ attributionLayout?: 'inline' | 'stacked';
/** Annotation footnotes generated by auto-thinning. Rendered as a numbered list above the source line. */
footnotes?: AnnotationFootnote[];
/** Reserved annotation-note and series-summary band, independent of the current demoted set. */
@@ -172,6 +177,8 @@ export interface AxisTick {
/** A single gridline with computed positions. */
export interface Gridline {
+ /** Optional resolved semantic stroke, such as an authored zero guide. */
+ color?: string;
/** Pixel position along the axis. */
position: number;
/** Whether this is a major or minor gridline. */
@@ -222,7 +229,9 @@ export interface AxisLayout {
/** Extent in px beyond the chart-area edge occupied by tick labels + title. Engine-resolved from the final (post-thinning) tick set. */
extent?: number;
/** Absolute anchor for the axis title. Replaces renderer-side offset math. */
- titlePosition?: { x: number; y: number; angle?: number };
+ titlePosition?: { x: number; y: number; angle?: number; textAnchor?: 'start' | 'middle' | 'end' };
+ /** Engine-wrapped title lines, used by a measured header band. */
+ titleLines?: string[];
}
// ---------------------------------------------------------------------------
@@ -360,6 +369,12 @@ export interface AreaMark {
y: number;
datum: Record;
tooltip?: TooltipContent;
+ /** False for synthetic pivot rows; geometry does not imply an observed zero. */
+ observed?: boolean;
+ /** Lower band boundary in pixels; y remains the upper boundary for interactions. */
+ yBottom?: number;
+ /** Contiguous area segment identity, split by explicit missing observations. */
+ segment?: number;
}>;
/** Accessibility attributes. */
aria: MarkAria;
@@ -688,6 +703,11 @@ export interface ResolvedLabel {
endpoint?: Point;
/** Author's from-end nudge, re-applied on every geometry rebuild (mirrors `endpoint` for `to`). */
fromOffset?: { dx: number; dy: number };
+ /**
+ * The target's own value label. The data end stops short of it instead of
+ * striking through it; retained so geometry rebuilds stop at the same label.
+ */
+ targetLabel?: Rect;
/** Fixed label attachment for outside-reference gutter leaders; retained during animation. */
labelAttachment?: 'left-center';
/** Connector line color. */
@@ -786,6 +806,10 @@ export interface ResolvedAnnotation {
/** A footnote entry generated by annotation auto-thinning. */
export interface AnnotationFootnote {
+ /** Original subtitle, kept separate from the primary text. */
+ subtitle?: string;
+ /** Parsed display runs, one array per wrapped line, including the ordinal. */
+ spans?: Array>;
/** Engine-wrapped display lines, including the number; omitted for one-line notes. */
lines?: string[];
/** Source annotation index, independent of note numbering. */
@@ -810,6 +834,8 @@ export interface LegendEntry {
color: string;
/** Shape of the swatch (matches the mark type). */
shape: 'circle' | 'square' | 'line';
+ /** SVG dash pattern for a `line` swatch, matching a strokeDash-encoded series. */
+ strokeDasharray?: string;
/** Whether this entry is currently highlighted/active. */
active?: boolean;
/** True for overflow indicator entries ("+N more"). Not interactive. */
@@ -839,6 +865,15 @@ export interface BaseLegendLayout {
position: 'top' | 'top-left' | 'right' | 'bottom' | 'bottom-right' | 'inline';
/** Bounding box for the legend (pixel coordinates). */
bounds: Rect;
+ /** Measured static key text in figure coordinates; absent on traditional chart keys. */
+ textRows?: Array<{
+ text: string;
+ x: number;
+ y: number;
+ style: TextStyle;
+ role: 'title' | 'unit' | 'context' | 'midpoint';
+ anchor?: 'start' | 'middle' | 'end';
+ }>;
/** Entry label style. */
labelStyle: TextStyle;
}
@@ -946,6 +981,8 @@ export interface ContinuousLegendLayout extends BaseLegendLayout {
mode: 'gradient' | 'binned';
/** The color bar rectangle (pixel coordinates). */
bar: Rect;
+ /** Optional measured vertical class rows for geographic side keys. */
+ rangeEntries?: Array<{ index: number; bounds: Rect }>;
/** Gradient color stops (empty in binned mode). */
colorStops: GradientColorStop[];
/** Class swatches, left to right (empty in gradient mode). */
@@ -1055,6 +1092,12 @@ export type LegendLayout =
/** A single resolved endpoint-label entry, fully positioned. */
export interface EndpointLabelEntry {
+ /** Actual observation x in pixels; leaders must not imply a shared final date. */
+ dataX?: number;
+ /** Source observation retained without parsing display text. */
+ source?: { datum: Record; x: unknown; observed: true };
+ /** Resolved date qualification for unequal series endpoints. */
+ date?: string;
/** Series identifier (matches mark.seriesKey). */
seriesKey: string;
/** Pre-wrapped label text lines. */
@@ -1063,6 +1106,8 @@ export interface EndpointLabelEntry {
value: string;
/** Series color. */
color: string;
+ /** SVG dash pattern of the series line, drawn on the swatch. */
+ strokeDasharray?: string;
/** True pixel y of the last data point on the line. */
dataY: number;
/** Displaced y after the bidirectional collision sweep. */
@@ -1085,6 +1130,16 @@ export interface EndpointLabelEntry {
/** Resolved layout for the endpoint labels column. */
export interface EndpointLabelsLayout {
+ /** Fit-tested, series-name-only labels in stacked bands. */
+ interior?: Array<{
+ seriesKey: string;
+ text: string;
+ x: number;
+ y: number;
+ color: string;
+ width: number;
+ height: number;
+ }>;
/** Per-series resolved entries. */
entries: EndpointLabelEntry[];
/** Bounding box for the column. */
@@ -1129,7 +1184,14 @@ export interface TooltipField {
*/
emphasis?: boolean;
/** Semantic role. `'total'` is the stack sum row, rendered below a hairline. */
- role?: 'total';
+ role?: 'x' | 'series' | 'value' | 'total';
+ source?: {
+ field: string;
+ channel: string;
+ tooltipIndex?: number;
+ layer?: string;
+ panel?: string;
+ };
}
/** Tooltip content descriptor for a data point. */
@@ -1310,7 +1372,7 @@ export interface ScaleInvert {
topData: number;
bottomData: number;
/** Scale type so the vanilla layer applies the correct inverse transform. */
- scaleType?: 'linear' | 'log' | 'pow' | 'sqrt' | 'symlog' | 'time';
+ scaleType?: 'linear' | 'log' | 'pow' | 'sqrt' | 'symlog' | 'time' | 'utc';
/** For band/point scales: ordered domain values and their pixel centers. */
domain?: (string | number)[];
positions?: number[];
@@ -1656,6 +1718,10 @@ export interface TableTotalRow {
* pagination, sort, search state, and accessibility metadata.
*/
export interface TableLayout {
+ /** Width used to resolve the portable attribution footer. */
+ width?: number;
+ /** Same measurement used for footer wrapping in the compiler. */
+ measureText?: MeasureTextFn;
/** Resolved chrome text elements. */
chrome: ResolvedChrome;
/** Resolved column definitions. */
@@ -1676,11 +1742,6 @@ export interface TableLayout {
striped: boolean;
/** Totals footer, present only when `totalRow` is enabled. */
totalRow?: TableTotalRow;
- /**
- * Whether condensed density is active.
- * @deprecated Read `density === 'condensed'` instead.
- */
- compact: boolean;
/** Accessibility metadata. */
a11y: { caption: string; summary: string };
/** The resolved theme. */
@@ -2066,6 +2127,12 @@ export interface GeoMapBorders {
* accessibility metadata.
*/
export interface GeoMapLayout {
+ /** Optional independent geographic viewports; absent on the legacy fast path. */
+ views?: import('./geo-views').GeoViewLayout[];
+ /** Optional map-only color key presentation; omitted when disabled. */
+ colorKeys?: import('./geo-color-key').GeoColorKeyDescriptor;
+ /** Omitted entirely unless geographic labels or callouts are authored. */
+ labels?: GeoLabelsBundle;
/** The map drawing area (after chrome and legend are subtracted). */
area: Rect;
/** Resolved chrome text elements with positions and styles. */
@@ -2082,7 +2149,8 @@ export interface GeoMapLayout {
pointContinuousLegend: ContinuousLegendLayout | null;
/**
* Nested-circle size key for the point layer's `size` channel (null when the
- * layer has no size encoding). Drawn bottom-right over the map.
+ * layer has no size encoding). Placed in a measured map corner that avoids
+ * occupied feature/point bounds; bottom-right wins clear or tied candidates.
*/
pointSizeLegend: SizeLegendLayout | null;
/** Continuous color legend for quantitative mode (null if hidden or categorical). */
@@ -2207,7 +2275,9 @@ export interface CompileOptions {
* How chrome height interacts with the height budget. Mirrors the spec-level
* `chromeLayout`; the engine reads whichever is set (spec wins).
*
- * - `'subtract'` (default): `height` is the fixed total; chrome shrinks the plot.
+ * - omitted: notes and the series summary grow the SVG; title, subtitle, and
+ * source subtract from `height`.
+ * - `'subtract'`: `height` is the fixed total; all chrome shrinks the plot.
* - `'grow'`: `height` is the plot budget; the SVG grows by the chrome height.
*/
chromeLayout?: 'subtract' | 'grow';
@@ -2267,34 +2337,6 @@ export interface CompileOptions {
* If not provided, the engine falls back to heuristic estimation.
*/
measureText?: MeasureTextFn;
- /** Extra pixels to reserve on the right margin for a secondary y-axis. Set by compileLayer when resolve.scale.y is 'independent'. */
- rightAxisReserve?: number;
- /**
- * Internal note-band reserve resolved from source annotation capacity and
- * responsive series-summary lines. Subtract mode consumes the fixed figure
- * budget; grow mode adds the reserve to the SVG height while keeping the
- * original layout budget and padding. Callers should not set this field.
- */
- footnoteReserve?: number;
- /** Recursion depth of the footnote-reserve convergence pass. Internal to compileChart. */
- footnotePass?: number;
- /** Set after facet height growth to prevent unbounded recursion. Internal to compileFaceted. */
- facetHeightGrown?: boolean;
- /**
- * Use this chart drawing area instead of computing one from chrome, axes,
- * and legend reservations. Set by compileLayer when compiling leaf layers so
- * their marks land in the primary layout's coordinate space; leaf specs lack
- * the layer-level chrome/legend/theme and would otherwise compute a
- * different area than the one the axes are rendered in.
- */
- frozenChartArea?: Rect;
- /**
- * Marks this compile as one leaf (or the primary spec) of a multi-leaf layer.
- * Set by compileLayer; callers should not set it. Only consumer today is the
- * canvas mark-layer resolution, which refuses canvas mode for layered charts
- * because the layer layout concatenates marks from several compiles.
- */
- layered?: boolean;
}
/** Extended compile options for table visualizations. */
diff --git a/packages/core/src/types/reader-slices.ts b/packages/core/src/types/reader-slices.ts
new file mode 100644
index 00000000..d9eb2516
--- /dev/null
+++ b/packages/core/src/types/reader-slices.ts
@@ -0,0 +1,82 @@
+import type { DatasetBinding, FieldMetadata, ReaderObservation } from './design';
+
+export type ReaderDomainValue =
+ | { kind: 'string'; value: string }
+ | { kind: 'number'; value: number }
+ | { kind: 'boolean'; value: boolean }
+ | { kind: 'date'; value: string };
+export interface ReaderBindingScope {
+ dataset: string;
+ transform: string;
+ grouping: string[];
+ xField: string;
+ valueField: string;
+}
+export interface ReaderPanelScope {
+ id: string;
+ facet?: { field: string; values: ReaderDomainValue[] };
+}
+export interface ReaderSeries {
+ key: string;
+ label: string;
+ color?: string;
+ valueField: string;
+ metadata?: FieldMetadata;
+}
+export interface ReaderSliceMember {
+ series: ReaderSeries;
+ status: 'observed' | 'no-observation' | 'ambiguous';
+ observations: ReaderObservation[];
+ value?: number;
+ valueStatus?: 'finite' | 'missing' | 'invalid';
+ formattedValue?: string;
+}
+export interface ReaderSlice {
+ kind: 'slice';
+ key: string;
+ binding: ReaderBindingScope;
+ panel: ReaderPanelScope;
+ x: ReaderDomainValue;
+ xLabel: string;
+ revision: string;
+ members: ReaderSliceMember[];
+ persistent: boolean;
+ state: 'visible' | 'filtered';
+}
+export interface ReaderSliceSelection {
+ kind: 'slices';
+ preview: ReaderSlice | null;
+ pinned: ReaderSlice[];
+ announcement: string;
+}
+export interface ReaderChangeMeta {
+ reason: 'user' | 'data' | 'filter' | 'programmatic';
+}
+export interface ReaderComparisonScope {
+ dataset: string;
+ transform: string;
+ grouping: string[];
+ panelId: string;
+}
+export interface ReaderComparisonOperand {
+ value: number;
+ metadata?: FieldMetadata;
+ binding: ReaderComparisonScope;
+}
+export type ReaderComparisonResult =
+ | {
+ status: 'comparable';
+ absolute: number;
+ relative?: number;
+ unit: string;
+ relativeUnavailable?: 'zero-baseline' | 'negative-baseline';
+ }
+ | {
+ status: 'unavailable';
+ reason: 'nonfinite' | 'missing-metadata' | 'incompatible-metadata' | 'incompatible-binding';
+ message: string;
+ };
+export interface ReaderSliceIndexOptions {
+ revision: string;
+ binding?: DatasetBinding;
+}
diff --git a/packages/core/src/types/spec.ts b/packages/core/src/types/spec.ts
index 398afd01..9d8aaae3 100644
--- a/packages/core/src/types/spec.ts
+++ b/packages/core/src/types/spec.ts
@@ -11,6 +11,7 @@
// Re-import for use in LegendConfig and overrides (avoids circular by importing from sibling)
import type { Breakpoint, LegendPosition } from '../responsive/breakpoints';
import type { FieldMetadata } from './design';
+import type { GeoDetailLayer, GeoInset } from './geo-views';
import type { SortState } from './layout';
import type { ColumnConfig } from './table';
import type { SeriesStrategy, TokenValue } from './theme';
@@ -179,7 +180,7 @@ export interface TrendlineConfig {
* When it's an object, it can include properties like interpolation,
* point markers, orientation, and more.
*/
-export interface MarkDef {
+interface MarkOptions {
/** The mark type. */
type: MarkType;
/**
@@ -410,6 +411,64 @@ export interface MarkDef {
majorityLine?: boolean | { seats?: number; label?: string };
}
+/** Shared visual options accepted by every authored mark. */
+type SharedMarkOptions = Pick<
+ MarkOptions,
+ | 'size'
+ | 'filled'
+ | 'opacity'
+ | 'fillOpacity'
+ | 'fill'
+ | 'stroke'
+ | 'strokeWidth'
+ | 'tooltip'
+ | 'clip'
+ | 'fillPattern'
+>;
+
+/**
+ * Authored mark configuration. Each mark accepts only options it understands.
+ * String marks remain shorthand for `{ type: mark }`.
+ * Extend this union with `type CustomMark = MarkDef & { ... }`, not an interface.
+ */
+export type MarkDef =
+ | (SharedMarkOptions & { type: 'bar' } & Pick)
+ | (SharedMarkOptions & { type: 'line' } & Pick)
+ | (SharedMarkOptions & { type: 'area' } & Pick)
+ | (SharedMarkOptions & { type: 'point' } & Pick)
+ | (SharedMarkOptions & { type: 'circle' })
+ | (SharedMarkOptions & { type: 'arc' } & Pick<
+ MarkOptions,
+ 'innerRadius' | 'outerRadius' | 'startAngle' | 'endAngle' | 'centerLabel'
+ >)
+ | (SharedMarkOptions & { type: 'text' } & Pick<
+ MarkOptions,
+ 'dx' | 'dy' | 'align' | 'baseline' | 'fontSize'
+ >)
+ | (SharedMarkOptions & { type: 'rule' })
+ | (SharedMarkOptions & { type: 'tick' })
+ | (SharedMarkOptions & { type: 'rect' } & Pick)
+ | (SharedMarkOptions & { type: 'lollipop' })
+ | (SharedMarkOptions & { type: 'beeswarm' })
+ | (SharedMarkOptions & { type: 'range' } & Pick)
+ | (SharedMarkOptions & { type: 'waffle' } & Pick<
+ MarkOptions,
+ 'units' | 'columns' | 'other' | 'cornerRadius'
+ >)
+ | (SharedMarkOptions & { type: 'calendar' } & Pick)
+ | (SharedMarkOptions & { type: 'parliament' } & Pick<
+ MarkOptions,
+ 'shape' | 'seatRadius' | 'majorityLine'
+ >)
+ | (SharedMarkOptions & { type: 'histogram' } & Pick<
+ MarkOptions,
+ 'orient' | 'cornerRadius' | 'binCount' | 'normalize'
+ >)
+ | (SharedMarkOptions & { type: 'density' } & Pick<
+ MarkOptions,
+ 'point' | 'interpolate' | 'bandwidth' | 'cumulative' | 'steps'
+ >);
+
// ---------------------------------------------------------------------------
// Encoding
// ---------------------------------------------------------------------------
@@ -461,6 +520,10 @@ export interface AxisConfig {
ticks?: boolean;
/** Axis position offset in pixels. */
offset?: number;
+ /** Angle of the title in degrees. A y-axis title at angle 0 and anchor end becomes a header. */
+ titleAngle?: number;
+ /** Position along the axis for the title; omission keeps the centered title. */
+ titleAnchor?: 'start' | 'middle' | 'end';
/** Padding between axis title and axis. */
titlePadding?: number;
/** Padding between tick labels and axis. */
@@ -812,9 +875,8 @@ export interface Encoding {
/**
* Angular position (slice value) for arc marks (pie/donut), the canonical
* value channel in v8. Waffle and parliament marks accept it too (the same
- * part-to-whole value channel). `y` is still accepted on these marks as a
- * deprecated alias for backward compatibility (a compile warning is emitted
- * and `y` is populated from `theta` internally). Not used by any other mark
+ * part-to-whole value channel). The compiler populates internal `y` from
+ * `theta`; authored `y` aliases are rejected. Not used by any other mark
* type.
*/
theta?: EncodingChannel;
@@ -1040,7 +1102,7 @@ export interface ConnectorConfig {
type: ConnectorType;
/** Curve displacement along the chord normal, from -1 to 1. Omitted or zero preserves the automatic curve. Ignored for other connector types. */
bend?: number;
- /** Whether to draw an arrowhead at the data-point end. Default: true for curve, false for straight. Drop-line never takes a head. */
+ /** Whether to draw an arrowhead at the data-point end. Default: false for both curve and straight. Drop-line never takes a head. */
arrow?: boolean;
}
@@ -1048,7 +1110,11 @@ export interface ConnectorConfig {
export interface AnnotationDot {
/** Circle radius in pixels. Default 4. */
radius?: number;
- /** Fill color. Defaults to theme background for an "open ring" look. */
+ /**
+ * Fill color. Defaults to `theme.colors.annotationMarkerFill`, which resolves to
+ * the theme's opaque surface. A default dot on an endpoint marker takes the
+ * marker's fill and stroke.
+ */
fill?: string;
/** Stroke color. Defaults to the connector's resolved stroke, so marker and leader read as one system. */
stroke?: string;
@@ -1060,8 +1126,6 @@ export interface AnnotationDot {
interface AnnotationBase {
/** Stable identifier for selection and edit callbacks. When provided, edit events include this ID for reliable element matching. */
id?: string;
- /** Human-readable label for the annotation. */
- label?: string;
/** Fill color for the annotation element. */
fill?: string;
/** Stroke color for the annotation element. */
@@ -1099,14 +1163,14 @@ export interface TextAnnotation extends AnnotationBase {
* Used for supporting context (e.g. methodology, source). Newlines in
* `text` still produce multi-line primary; subtitle is a separate block.
*
- * Supports the same inline `**bold**` spans as `text`. When a subtitle is
- * present and no `fontWeight` is set, the primary `text` resolves to weight
- * 700 — the lede + context stack.
+ * Supports the same inline `**bold**` spans as `text`. The primary `text`
+ * keeps its own weight (default 400); use `fontWeight` or `**bold**` spans
+ * for a bold lede.
*/
subtitle?: string;
- /** Subtitle size in pixels. Default: round(fontSize * 0.85). */
+ /** Subtitle size in pixels. Default: round(fontSize * 12 / 13). */
subtitleFontSize?: number;
- /** Optional display wrap width in pixels. Omitted or null keeps unbounded lines. */
+ /** Display wrap width in pixels. Omitted uses min(240, plot width minus twice the padding); null disables wrapping. */
maxWidth?: number | null;
/**
* Open-ring marker drawn on the data point itself (not on the pulled-back
@@ -1133,7 +1197,7 @@ export interface TextAnnotation extends AnnotationBase {
* Connector from label to anchor point.
* - `true` (default): straight line, no arrow
* - `'straight'`: straight line, no arrow
- * - `'curve'`: curved arrow with arrowhead
+ * - `'curve'`: curved leader; set arrow: true for an arrowhead
* - `'drop-line'`: vertical line through the data point's x
* - `{ type, arrow? }`: object form for explicit arrow control
* - `false`: no connector
@@ -1169,6 +1233,8 @@ export interface TextAnnotation extends AnnotationBase {
*/
export interface RangeAnnotation extends AnnotationBase {
type: 'range';
+ /** Text drawn with the range band. */
+ label?: string;
/** Start of the range on the x-axis. */
x1?: string | number;
/** End of the range on the x-axis. */
@@ -1191,7 +1257,7 @@ export interface RangeAnnotation extends AnnotationBase {
labelOffset?: AnnotationOffset;
/** Anchor direction for the range label. */
labelAnchor?: AnnotationAnchor;
- /** Font size override for the range label (px). Default: 11. */
+ /** Font size override for the range label (px). Default: theme small (12px). */
fontSize?: number;
/** Font weight override for the range label. Default: 500. */
fontWeight?: number;
@@ -1202,14 +1268,10 @@ export interface RangeAnnotation extends AnnotationBase {
* Useful for baselines (zero), targets, or thresholds.
*/
export interface RefLineAnnotation extends AnnotationBase {
- /**
- * Discriminant. `'refline'` is canonical and the only value in the type.
- * `'rule'` (it collides with the `rule` mark type) is still accepted at
- * runtime for backward compatibility -- the spec-sugar pass rewrites it to
- * `'refline'` with a deprecation warning before validation -- but it is not
- * part of this type. Use `'refline'`.
- */
+ /** Reference-line annotation discriminant. Older rule aliases require migrateSpecV8. */
type: 'refline';
+ /** Text drawn with the reference line. */
+ label?: string;
/** X-axis value for a vertical reference line. */
x?: string | number;
/** Y-axis value for a horizontal reference line. */
@@ -1222,11 +1284,11 @@ export interface RefLineAnnotation extends AnnotationBase {
strokeWidth?: number;
/** Pixel offset for the reference line label. */
labelOffset?: AnnotationOffset;
- /** Opt-in screen-space label placement. Outside reserves a right gutter or top lane; omission preserves legacy labelAnchor behavior. */
+ /** Screen-space label placement. Outside reserves a right gutter or top lane; omission uses auto unless labelAnchor is authored. */
labelPlacement?: 'auto' | 'start' | 'end' | 'outside';
/** Anchor direction for the reference line label. */
labelAnchor?: AnnotationAnchor;
- /** Label font size in pixels. Default: 11. */
+ /** Label font size in pixels. Default: theme small (12px). */
fontSize?: number;
/** Label font weight. Default: 400. */
fontWeight?: number;
@@ -1256,8 +1318,7 @@ export type Annotation = TextAnnotation | RangeAnnotation | RefLineAnnotation;
export type DarkMode = 'auto' | 'force' | 'off';
/**
- * Per-element chrome theme override. Pass a plain string (color only,
- * backward-compatible) or an object with full typography control.
+ * Per-element chrome theme typography controls.
*/
export interface ChromeThemeOverride {
/** Text color. Accepts a TokenValue for light/dark pairs. */
@@ -1322,6 +1383,12 @@ export interface ThemeConfig {
annotationMarkerFill?: TokenValue;
/** Annotation plate color. Accepts a TokenValue light/dark pair. */
annotationPlate?: TokenValue;
+ /** Optional map missing-data and geographic border ink. */
+ gridlineZero?: TokenValue;
+ mapNoDataFill?: TokenValue;
+ mapNoDataHatch?: TokenValue;
+ mapInteriorBorder?: TokenValue;
+ mapOutlineBorder?: TokenValue;
/** Semantic color for positive/up-trend values. Accepts a TokenValue for light/dark pairs. */
positive?: TokenValue;
/** Semantic color for negative/down-trend values. Accepts a TokenValue for light/dark pairs. */
@@ -1341,14 +1408,17 @@ export interface ThemeConfig {
subtitle?: number;
/** Body text (tooltips, legend labels). Default: 13. */
body?: number;
- /** Small text (source line, footer). Default: 11. */
+ /** Small text (source line, footer). Default: 12. */
small?: number;
- /** Axis tick labels. Default: 11. */
+ /** Axis tick labels. Default: 12. */
axisTick?: number;
/** KPI metric uppercase label. Default: 10. */
metricLabel?: number;
/** KPI metric primary value. Default: 22. Delta/secondary derive from this. */
metricValue?: number;
+ /** Series-name and numerical endpoint font sizes in pixels. */
+ endpointLabel?: number;
+ endpointValue?: number;
};
/** Font weight overrides. Partial — only specified keys are overridden. */
weights?: {
@@ -1378,27 +1448,29 @@ export interface ThemeConfig {
xAxisHeight?: number;
/** Gap in pixels between the x-axis line and tick label text. Increase when larger axisTick fonts sit too close to the axis line. */
xAxisLabelPadding?: number;
+ /** Endpoint block separation, internal value gap, and plot-to-column gap. */
+ endpointEntryGap?: number;
+ endpointValueGap?: number;
+ endpointColumnGap?: number;
};
/** Border radius for chart container and tooltips. */
borderRadius?: number;
/**
- * Per-element chrome style overrides. Pass a plain string for color-only
- * (backward-compatible), or a ChromeThemeOverride object for full
- * typography control (color, fontSize, fontWeight, lineHeight).
+ * Per-element chrome typography control (color, fontSize, fontWeight, lineHeight).
*/
chrome?: {
/** Eyebrow (kicker) style override. */
- eyebrow?: ChromeThemeOverride | /** @deprecated Use { color }. */ string;
+ eyebrow?: ChromeThemeOverride;
/** Title style override. */
- title?: ChromeThemeOverride | /** @deprecated Use { color }. */ string;
+ title?: ChromeThemeOverride;
/** Subtitle style override. */
- subtitle?: ChromeThemeOverride | /** @deprecated Use { color }. */ string;
+ subtitle?: ChromeThemeOverride;
/** Source/attribution style override. */
- source?: ChromeThemeOverride | /** @deprecated Use { color }. */ string;
+ source?: ChromeThemeOverride;
/** Byline style override. */
- byline?: ChromeThemeOverride | /** @deprecated Use { color }. */ string;
+ byline?: ChromeThemeOverride;
/** Footer style override. */
- footer?: ChromeThemeOverride | /** @deprecated Use { color }. */ string;
+ footer?: ChromeThemeOverride;
};
/**
* Editorial rule above the chrome block: a short colored bar (the
@@ -1406,12 +1478,12 @@ export interface ThemeConfig {
*/
rule?: {
/** Rule color. Accepts a TokenValue for light/dark pairs. */
- color: TokenValue;
+ color?: TokenValue;
/** Rule length in pixels. */
- width: number;
+ width?: number;
/** Rule height in pixels. */
- thickness: number;
- };
+ thickness?: number;
+ } | null;
/**
* Series color assignment strategy.
* Default: 'palette' (full categorical palette always, zero visual drift).
@@ -1427,13 +1499,18 @@ export interface ThemeConfig {
* Label density mode controlling how many data labels are shown.
* - 'all': show every label, skip collision detection
* - 'auto': show labels with collision detection (default)
- * - 'endpoints': show only first and last per series (useful for line charts)
+ * - 'endpoints': show only first and last per series
+ *
+ * Line and area marks draw no value labels; identify their series with
+ * `endpointLabels` or the legend.
* - 'none': hide all labels (rely on tooltips and legend)
*/
export type LabelDensity = 'all' | 'auto' | 'endpoints' | 'none';
/** Label display configuration for chart data labels. */
export interface LabelConfig {
+ /** Placement for bar/column values. Column is only supported for simple horizontal bars. */
+ placement?: 'adaptive' | 'inside' | 'outside' | 'column';
/** How many labels to show. Defaults to 'auto'. */
density?: LabelDensity;
/** Number format override for label values (d3-format string, e.g. ",.0f"). */
@@ -1444,12 +1521,6 @@ export interface LabelConfig {
suffix?: string;
/** Fixed CSS color for all labels. Overrides the default fill-derived color. */
color?: string;
- /**
- * Per-series pixel offsets for fine-tuning label positions, keyed by series name.
- * @deprecated Per-series pixel offsets are a workaround for collision bugs
- * now fixed by the layout engine. Will be removed in a future version.
- */
- offsets?: Record;
/** Font size in pixels for bar/column value labels. */
fontSize?: number;
}
@@ -1526,19 +1597,18 @@ export type EndpointLabelContent = 'label' | 'value' | 'label value';
* for multi-series line/area charts. Each entry pairs the series name with its
* last formatted value, optionally anchored to the line by an open-circle marker.
*
- * The column is independent of the traditional `legend` and the legacy
- * end-of-line labels. Together with `legend.show`, the three suppression toggles
- * follow this truth table for ≥2-series line/area charts:
+ * The column and traditional legend are independent identification surfaces.
+ * For multi-series line/area charts:
*
- * | `legend.show` | `endpointLabels` | Traditional legend | Endpoint column | End-of-line labels |
- * |--|--|--|--|--|
- * | unset | unset | hidden (auto-suppressed) | shown (default) | hidden |
- * | true | unset | shown | shown | hidden |
- * | unset | false | shown (auto-suppress revoked) | hidden | hidden |
- * | false | false | hidden | hidden | shown (last-resort) |
- * | true | false | shown | hidden | hidden |
- * | false | true | hidden | shown | hidden |
- * | true | true | shown | shown | hidden |
+ * | `legend.show` | `endpointLabels` | Traditional legend | Endpoint column |
+ * |--|--|--|--|
+ * | unset | unset | hidden | shown |
+ * | true | unset | shown | shown |
+ * | unset | false | shown | hidden |
+ * | false | false | hidden (accessibility warning) | hidden |
+ * | true | false | shown | hidden |
+ * | false | true | hidden | shown |
+ * | true | true | shown | shown |
*
* Single-series charts: column is hidden by default (nothing to identify).
*
@@ -1548,6 +1618,10 @@ export type EndpointLabelContent = 'label' | 'value' | 'label value';
* in `legend/__tests__/suppression.test.ts` enforce every cell.
*/
export interface EndpointLabelsConfig {
+ /** Stacked-area anchor: cumulative boundary or the middle of the band. Default band. */
+ anchor?: 'boundary' | 'band';
+ /** Series identification in the end column or fitted inside stacked bands. */
+ placement?: 'end' | 'interior';
/** Explicit on/off. When undefined, the chart auto-decides based on series count. */
show?: boolean;
/** Maximum number of series before endpoint labels switch to a legend. Default 8. */
@@ -1578,9 +1652,12 @@ export interface EndpointLabelsConfig {
format?: string;
/** Max wrap width in pixels for long series names. Default 96. */
width?: number;
- /** Render an open-circle marker on the line at the right edge. Default true. */
+ /**
+ * Render a ring marker centered on the line's end point. Default true. Its
+ * fill defaults to the opaque canvas color so the ring covers the line end.
+ */
showMarker?: boolean;
- /** Override the marker style. */
+ /** Override the marker style. `fill: 'transparent'` keeps a see-through ring. */
markerStyle?: {
fill?: string;
stroke?: string;
@@ -1627,11 +1704,7 @@ export interface SeriesStyle {
export type AnimationEase = 'smooth' | 'snappy';
/** Opt-in motion language. */
-export type AnimationPreset =
- | 'fluid'
- | 'subtle'
- /** @deprecated Omit preset for v8 timing. */
- | 'legacy';
+export type AnimationPreset = 'fluid' | 'subtle';
/** Physical spring parameters. Quantitative motion is bounded at critical damping. */
export interface SpringConfig {
@@ -1650,7 +1723,7 @@ export interface AnimationStagger {
/** Fine-grained responsive behavior configuration. */
export interface ResponsiveConfig {
- /** Maximum eligible inline callouts below 400px container width; null or omitted is unlimited. Pinned annotations are exempt. */
+ /** Maximum eligible inline callouts below 400px container width; default 1; null is unlimited. Pinned annotations are exempt. */
compactAnnotationLimit?: number | null;
/** Preserve the final temporal boundary before thinning interior ticks. */
preserveEnd?: boolean;
@@ -1743,6 +1816,8 @@ export interface ChartSpecOverride {
chrome?: Chrome;
/** Override label configuration at this breakpoint. */
labels?: LabelSpec;
+ /** Override endpoint labels at this breakpoint, including migrated v8 line labels. */
+ endpointLabels?: boolean | EndpointLabelsConfig;
/** Override legend configuration at this breakpoint. */
legend?: LegendConfig;
/** Override annotations at this breakpoint. */
@@ -1788,7 +1863,6 @@ export interface Metric {
* Encoding for arc marks (pie/donut charts).
* - `theta`: required (quantitative — the canonical slice value channel)
* - `color`: required (nominal/ordinal — the category)
- * - `y`: deprecated alias for `theta`, accepted for backward compatibility
*/
export interface ArcEncoding extends Encoding {
/** Canonical value channel for arc marks (quantitative — the slice value). */
@@ -1885,8 +1959,7 @@ export interface RangeEncoding extends Encoding
* Encoding for waffle marks (unit grids for part-to-whole counts).
* - `color`: required (nominal/ordinal, the category)
* - `theta`: the canonical quantitative share, the same part-to-whole value
- * channel arc marks use. `y` is a deprecated alias, accepted for backward
- * compatibility.
+ * channel arc marks use.
*/
export interface WaffleEncoding extends Encoding {
color: EncodingChannel | ConditionalValueDef;
@@ -1896,8 +1969,7 @@ export interface WaffleEncoding extends Encodin
* Encoding for parliament marks (hemicycle seat-dot charts).
* - `color`: required (nominal/ordinal, the party)
* - `theta`: the canonical quantitative seat count, the same part-to-whole
- * value channel arc/waffle marks use. `y` is a deprecated alias, accepted
- * for backward compatibility.
+ * value channel arc/waffle marks use.
*/
export interface ParliamentEncoding extends Encoding {
color: EncodingChannel | ConditionalValueDef;
@@ -2021,7 +2093,9 @@ interface BaseChartSpec {
/**
* How chrome height interacts with the container height budget.
*
- * - `'subtract'` (default): the container height is fixed and chrome text
+ * - omitted: only notes and the series summary grow the figure; title,
+ * subtitle, and source subtract from the height budget.
+ * - `'subtract'`: the container height is fixed and chrome text
* (title/subtitle/source) is subtracted from it, shrinking the plot. This
* is the historical behavior.
* - `'grow'`: the container height is treated as the *plot* budget, and the
@@ -2029,7 +2103,7 @@ interface BaseChartSpec {
* its full budget (minus axis/padding overhead) regardless of how many
* lines the title wraps to.
*
- * `'grow'` is a no-op for faceted specs in this version.
+ * Facets and layers report engine-owned growth once.
*/
chromeLayout?: 'subtract' | 'grow';
/**
@@ -2080,7 +2154,7 @@ interface BaseChartSpec {
* - omitted: auto-enable for multi-series line/area charts, hide otherwise.
*
* See {@link EndpointLabelsConfig} for the full suppression truth table that
- * relates this flag to `legend.show` and the legacy end-of-line labels.
+ * relates this flag to `legend.show`.
*/
endpointLabels?: boolean | EndpointLabelsConfig;
/**
@@ -2091,9 +2165,6 @@ interface BaseChartSpec {
responsive?: boolean | ResponsiveConfig;
/** Theme configuration overrides. */
theme?: ThemeConfig;
- /** Dark mode behavior. Defaults to "off". */
- /** @deprecated Use the mount darkMode option; the spec field has no effect. */
- darkMode?: DarkMode;
/**
* Whether to show the OpenData watermark. Defaults to true; auto-hides
* when the container is under 200px tall (the cramped height class) unless
@@ -2213,75 +2284,75 @@ interface BaseChartSpec {
*/
export type ChartSpec =
| (BaseChartSpec & {
- mark: 'arc' | (MarkDef & { type: 'arc' });
+ mark: 'arc' | Extract;
encoding: ArcEncoding;
})
| (BaseChartSpec & {
- mark: 'line' | (MarkDef & { type: 'line' });
+ mark: 'line' | Extract;
encoding: LineEncoding;
})
| (BaseChartSpec & {
- mark: 'bar' | (MarkDef & { type: 'bar' });
+ mark: 'bar' | Extract;
encoding: BarEncoding;
})
| (BaseChartSpec & {
- mark: 'area' | (MarkDef & { type: 'area' });
+ mark: 'area' | Extract;
encoding: AreaEncoding;
})
| (BaseChartSpec & {
- mark: 'point' | (MarkDef & { type: 'point' });
+ mark: 'point' | Extract;
encoding: PointEncoding;
})
| (BaseChartSpec & {
- mark: 'circle' | (MarkDef & { type: 'circle' });
+ mark: 'circle' | Extract;
encoding: CircleEncoding;
})
| (BaseChartSpec & {
- mark: 'lollipop' | (MarkDef & { type: 'lollipop' });
+ mark: 'lollipop' | Extract;
encoding: LollipopEncoding;
})
| (BaseChartSpec & {
- mark: 'beeswarm' | (MarkDef & { type: 'beeswarm' });
+ mark: 'beeswarm' | Extract;
encoding: BeeswarmEncoding;
})
| (BaseChartSpec & {
- mark: 'range' | (MarkDef & { type: 'range' });
+ mark: 'range' | Extract;
encoding: RangeEncoding;
})
| (BaseChartSpec & {
- mark: 'waffle' | (MarkDef & { type: 'waffle' });
+ mark: 'waffle' | Extract;
encoding: WaffleEncoding;
})
| (BaseChartSpec & {
- mark: 'text' | (MarkDef & { type: 'text' });
+ mark: 'text' | Extract;
encoding: TextEncoding;
})
| (BaseChartSpec & {
- mark: 'tick' | (MarkDef & { type: 'tick' });
+ mark: 'tick' | Extract;
encoding: TickEncoding;
})
| (BaseChartSpec & {
- mark: 'rect' | (MarkDef & { type: 'rect' });
+ mark: 'rect' | Extract;
encoding: RectEncoding;
})
| (BaseChartSpec & {
- mark: 'rule' | (MarkDef & { type: 'rule' });
+ mark: 'rule' | Extract;
encoding: Encoding;
})
| (BaseChartSpec & {
- mark: 'calendar' | (MarkDef & { type: 'calendar' });
+ mark: 'calendar' | Extract;
encoding: CalendarEncoding;
})
| (BaseChartSpec & {
- mark: 'parliament' | (MarkDef & { type: 'parliament' });
+ mark: 'parliament' | Extract;
encoding: ParliamentEncoding;
})
| (BaseChartSpec & {
- mark: 'histogram' | (MarkDef & { type: 'histogram' });
+ mark: 'histogram' | Extract;
encoding: HistogramEncoding;
})
| (BaseChartSpec & {
- mark: 'density' | (MarkDef & { type: 'density' });
+ mark: 'density' | Extract;
encoding: DensityEncoding;
});
@@ -2311,9 +2382,6 @@ export interface TableSpec {
chrome?: Chrome;
/** Theme configuration overrides. */
theme?: ThemeConfig;
- /** Dark mode behavior. */
- /** @deprecated Use the mount darkMode option; the spec field has no effect. */
- darkMode?: DarkMode;
/** Whether to show the OpenData watermark. Defaults to true. */
watermark?: boolean;
/** Enable client-side search/filter. */
@@ -2338,10 +2406,6 @@ export interface TableSpec {
* "Total".
*/
totalRow?: boolean | { label?: string };
- /**
- * @deprecated Use `density: 'condensed'`. Still honored, with a warning.
- */
- compact?: boolean;
/** Whether the table adapts to container width. Defaults to true. */
responsive?: boolean;
/** Animation configuration for entrance animations. */
@@ -2473,9 +2537,6 @@ export interface GraphSpec {
annotations?: Annotation[];
/** Theme configuration overrides. */
theme?: ThemeConfig;
- /** Dark mode behavior. */
- /** @deprecated Use the mount darkMode option; the spec field has no effect. */
- darkMode?: DarkMode;
/**
* Motion configuration. DEFAULT-ON: omit for full defaults, `false` to opt
* out of choreography (layout warmup still applies).
@@ -2599,13 +2660,16 @@ export interface LayerSpec {
labels?: LabelSpec;
/** Legend display configuration. */
legend?: LegendConfig;
- /** Whether the chart adapts to container width. Defaults to true. */
- responsive?: boolean;
+ /**
+ * Responsive behavior for the composed chart, with the same meaning as
+ * `ChartSpec.responsive`: `true`/`false` enables/disables container-width
+ * adaptation, and a `ResponsiveConfig` object gives fine-grained control
+ * (e.g. `{ compactAnnotationLimit: null }`). When omitted, the first leaf
+ * layer's `responsive` applies.
+ */
+ responsive?: boolean | ResponsiveConfig;
/** Theme configuration overrides. */
theme?: ThemeConfig;
- /** Dark mode behavior. Defaults to "off". */
- /** @deprecated Use the mount darkMode option; the spec field has no effect. */
- darkMode?: DarkMode;
/** Whether to show the OpenData watermark. Defaults to true. */
watermark?: boolean;
/** Resolution strategy for shared scales/axes/legends. */
@@ -2696,23 +2760,10 @@ export interface SankeySpec {
legend?: LegendConfig;
/** Theme configuration overrides. */
theme?: ThemeConfig;
- /** Dark mode behavior. Defaults to "off". */
- /** @deprecated Use the mount darkMode option; the spec field has no effect. */
- darkMode?: DarkMode;
/** Whether to show the OpenData watermark. Defaults to true. */
watermark?: boolean;
/** Animation configuration for entrance animations. */
animation?: AnimationSpec;
- /**
- * d3-format string applied to flow values in tooltips and ARIA labels.
- * Uses the literal suffix extension: ".0f%" appends "%" to the formatted
- * number (data value 28 renders as "28%"). For currency: "$,.0f" or "$~s".
- * For SI suffixes: "~s" (renders 1000 as "1k"). When not set, values use
- * the default number formatter.
- * @deprecated Use the encoding channel's `format` field instead. This
- * property is expanded into the encoding during compilation.
- */
- valueFormat?: string;
}
// ---------------------------------------------------------------------------
@@ -2768,9 +2819,6 @@ export interface TileMapSpec {
legend?: LegendConfig;
/** Theme configuration overrides. */
theme?: ThemeConfig;
- /** Dark mode behavior. Defaults to "off". */
- /** @deprecated Use the mount darkMode option; the spec field has no effect. */
- darkMode?: DarkMode;
/** Whether to show the OpenData watermark. Defaults to true. */
watermark?: boolean;
/** Animation configuration for entrance animations. */
@@ -2820,6 +2868,14 @@ export interface GeoMapZoomConfig {
/** Geo configuration for map specs. */
export interface GeoMapGeo {
+ /** Primary topology object; omission retains first-object behavior. */
+ objectKey?: string;
+ /** Fit against this primary-topology object, independent of contextual detail. */
+ fitObjectKey?: string;
+ /** Named assertion of the decoded coordinate plane for identity detail. */
+ sourcePlane?: string;
+ detailLayers?: GeoDetailLayer[];
+ insets?: GeoInset[];
/** TopoJSON topology object. User imports this from us-atlas, world-atlas, or their own source. */
features: unknown;
/** Field in the TopoJSON feature properties to use as the join key. Defaults to 'id'. */
@@ -2850,12 +2906,36 @@ export interface GeoMapGeo {
zoom?: boolean | GeoMapZoomConfig;
}
+/** Explicit map anchor; feature and point identities occupy separate namespaces. */
+export type GeoAnchor = (
+ | { kind: 'feature'; id: string | number }
+ | { kind: 'point'; id: string | number }
+ | { kind: 'coordinate'; space: 'geographic' | 'source'; x: number; y: number }
+) & { viewId?: string; layerId?: string };
+
+/** Static authored geographic explanation; plain text, without chart editing semantics. */
+export interface GeoCallout {
+ id: string;
+ anchor: GeoAnchor;
+ text: string;
+ subtitle?: string;
+ priority?: number;
+ minZoom?: number;
+ maxZoom?: number;
+ offset?: { dx: number; dy: number };
+ maxWidth?: number;
+}
+
/** Encoding channels specific to map visualizations. */
export interface GeoMapEncoding {
/** Join key field in the data. Required for choropleth; optional in basemap-only mode (points layer with empty data). */
key?: EncodingChannel;
/** Color encoding (quantitative or nominal). Drives fill color of features. Optional when a points layer is present (basemap-only mode). */
color?: EncodingChannel;
+ /** Explicit feature label field, read from its joined row or same-named topology property. */
+ text?: EncodingChannel;
+ /** Higher finite quantitative values win label placement priority. Requires text. */
+ textPriority?: EncodingChannel;
/** Tooltip encoding (optional). */
tooltip?: EncodingChannel | EncodingChannel[];
}
@@ -2868,10 +2948,14 @@ export interface GeoMapPointsLayer {
longitude: EncodingChannel;
/** Field holding latitude (y coordinate). */
latitude: EncodingChannel;
- /** Optional quantitative size encoding. Area-proportional: r proportional to sqrt(value). No size legend is rendered in v1; explain the scale in chrome.subtitle if needed. */
+ /** Optional quantitative size encoding. Area-proportional: r proportional to sqrt(value). A proportional size key explains the resolved radius scale. */
size?: EncodingChannel;
/** Optional color encoding (nominal or quantitative). Independent scale from choropleth. */
color?: EncodingChannel;
+ /** Explicit point label field. Requires a unique points.key. */
+ text?: EncodingChannel;
+ /** Quantitative label priority; defaults to the point size value. Requires text. */
+ textPriority?: EncodingChannel;
/** Tooltip channel(s), same shape as GeoMapEncoding.tooltip. */
tooltip?: EncodingChannel | EncodingChannel[];
/** Stable id for event callbacks, and the key used to match points across
@@ -2894,24 +2978,20 @@ export interface GeoMapSpec {
encoding: GeoMapEncoding;
/** Editorial chrome (title, subtitle, source, byline, footer). */
chrome?: Chrome;
+ /** Optional static distribution and reader active-value presentation. Both default to false. */
+ colorKey?: { activeValue?: boolean; distribution?: boolean };
/** Legend display configuration. */
legend?: LegendConfig;
/** Theme configuration overrides. */
theme?: ThemeConfig;
- /** Dark mode behavior. Defaults to "off". */
- /** @deprecated Use the mount darkMode option; the spec field has no effect. */
- darkMode?: DarkMode;
/** Whether to show the OpenData watermark. Defaults to true. */
watermark?: boolean;
/** Optional point/symbol layer rendered above choropleth features. */
points?: GeoMapPointsLayer;
+ /** Authored labels with explicit feature, point, or coordinate anchors. */
+ callouts?: GeoCallout[];
/** Animation configuration for entrance animations. */
animation?: AnimationSpec;
- /**
- * d3-format string applied to color values in tooltips and legend.
- * @deprecated Use the encoding channel's `format` field instead.
- */
- valueFormat?: string;
}
// ---------------------------------------------------------------------------
@@ -2938,20 +3018,10 @@ export interface BarListSpec {
chrome?: Chrome;
/** Theme configuration overrides. */
theme?: ThemeConfig;
- /** Dark mode behavior. Defaults to "off". */
- /** @deprecated Use the mount darkMode option; the spec field has no effect. */
- darkMode?: DarkMode;
/** Whether to show the OpenData watermark. Defaults to true. */
watermark?: boolean;
/** Animation configuration for entrance animations. */
animation?: AnimationSpec;
- /**
- * d3-format string applied to bar values and tooltips.
- * Examples: ".1f" for one decimal, "$,.0f" for currency, "~s" for SI.
- * @deprecated Use the encoding channel's `format` field instead. This
- * property is expanded into the encoding during compilation.
- */
- valueFormat?: string;
}
/** Encoding channels for a barlist visualization. */
@@ -3456,14 +3526,9 @@ export function isRangeAnnotation(annotation: Annotation): annotation is RangeAn
return annotation.type === 'range';
}
-/**
- * Check if an annotation is a RefLineAnnotation. Also accepts the deprecated
- * `'rule'` type at runtime (pre-sugar-expansion input), since `annotation.type`
- * may not yet be narrowed to the canonical `'refline'` form.
- */
+/** Check if an annotation is a RefLineAnnotation. */
export function isRefLineAnnotation(annotation: Annotation): annotation is RefLineAnnotation {
- const type: string = annotation.type;
- return type === 'refline' || type === 'rule';
+ return annotation.type === 'refline';
}
// ---------------------------------------------------------------------------
diff --git a/packages/core/src/types/theme.ts b/packages/core/src/types/theme.ts
index ac288fc4..71dfeb77 100644
--- a/packages/core/src/types/theme.ts
+++ b/packages/core/src/types/theme.ts
@@ -66,6 +66,8 @@ export interface ThemeColors {
text: string;
/** Gridline color. */
gridline: string;
+ /** Zero-rule ink. Defaults to stronger contrast than ordinary gridlines. */
+ gridlineZero?: string;
/** Axis line and tick color. */
axis: string;
/** Annotation fill color. */
@@ -86,6 +88,14 @@ export interface ThemeColors {
annotationMarkerFill: string;
/** Annotation plate color. */
annotationPlate: string;
+ /** Missing-data map surface; omission derives from the current neutral ramp. */
+ mapNoDataFill?: string;
+ /** Missing-data map hatch ink; omission derives from the current neutral ramp. */
+ mapNoDataHatch?: string;
+ /** Interior geographic borders; omission uses the current mode-specific rule. */
+ mapInteriorBorder?: string;
+ /** Geographic outline ink; omission uses the current mode-specific rule. */
+ mapOutlineBorder?: string;
/** Semantic color for positive/up-trend values (e.g. sparkline trend coloring). */
positive: string;
/** Semantic color for negative/down-trend values (e.g. sparkline trend coloring). */
@@ -117,6 +127,10 @@ export interface ThemeFontSizes {
small: number;
/** Axis tick label font size in pixels. */
axisTick: number;
+ /** Series-name text in endpoint columns and interior area labels. */
+ endpointLabel: number;
+ /** Numerical endpoint value text. */
+ endpointValue: number;
/** KPI metric uppercase label font size in pixels. */
metricLabel: number;
/** KPI metric primary value font size in pixels. The delta and secondary
@@ -156,6 +170,12 @@ export interface ThemeFonts {
export interface ThemeSpacing {
/** Padding inside the visualization container (all sides). */
padding: number;
+ /** Minimum separation between resolved endpoint label blocks. */
+ endpointEntryGap: number;
+ /** Gap between the endpoint name and its numerical value. */
+ endpointValueGap: number;
+ /** Gap between plot bounds and the endpoint column. */
+ endpointColumnGap: number;
/** Gap between chrome elements (title to subtitle, subtitle to chart, etc.). */
chromeGap: number;
/** Gap between the last chrome element and the chart area. */
@@ -243,15 +263,14 @@ export interface ThemeBase {
seriesStrategy: SeriesStrategy;
}
-/** @deprecated Use ThemeConfig for input or ResolvedTheme for resolved output. */
-export type Theme = ThemeBase;
-
/** Fully resolved runtime theme, including color adaptation and token metadata. */
export interface ResolvedTheme extends ThemeBase {
/** Colors plus the neutral ramp derived from `text` and `background`. */
colors: ResolvedThemeColors;
/** Whether dark mode adaptations have been applied to this theme. */
isDark: boolean;
+ /** Authored theme leaves, retained for CSS overrides and mode adaptation. @internal */
+ _authored?: import('./spec').ThemeConfig;
/**
* Original light/dark token pairs from the user's ThemeConfig. Used
* by adaptTheme() to skip algorithmic adaptation for tokens with
diff --git a/packages/engine/src/__test-fixtures__/mark-options.ts b/packages/engine/src/__test-fixtures__/mark-options.ts
new file mode 100644
index 00000000..5fe802f1
--- /dev/null
+++ b/packages/engine/src/__test-fixtures__/mark-options.ts
@@ -0,0 +1,88 @@
+import type { MarkDef, MarkType } from '@opendata-ai/openchart-core';
+
+/** Independently specified authoring contract, checked by TypeScript and exercised
+ * against published JSON Schema and runtime diagnostics. */
+export const MARK_CONTRACT_TYPES = [
+ 'bar',
+ 'line',
+ 'area',
+ 'point',
+ 'circle',
+ 'arc',
+ 'text',
+ 'rule',
+ 'tick',
+ 'rect',
+ 'lollipop',
+ 'beeswarm',
+ 'range',
+ 'waffle',
+ 'calendar',
+ 'parliament',
+ 'histogram',
+ 'density',
+] as const satisfies readonly MarkType[];
+
+export const MARK_OPTION_CASES = {
+ orient: { value: 'horizontal', marks: ['bar', 'histogram'] },
+ cornerRadius: { value: 3, marks: ['bar', 'rect', 'histogram', 'waffle'] },
+ point: { value: true, marks: ['line', 'area', 'density'] },
+ interpolate: { value: 'linear', marks: ['line', 'area', 'density'] },
+ innerRadius: { value: 20, marks: ['arc'] },
+ outerRadius: { value: 80, marks: ['arc'] },
+ startAngle: { value: 0, marks: ['arc'] },
+ endAngle: { value: 3.14, marks: ['arc'] },
+ centerLabel: { value: 'Total', marks: ['arc'] },
+ binCount: { value: 8, marks: ['histogram'] },
+ normalize: { value: true, marks: ['histogram'] },
+ bandwidth: { value: 2, marks: ['density'] },
+ cumulative: { value: true, marks: ['density'] },
+ steps: { value: 25, marks: ['density'] },
+ trendline: { value: true, marks: ['point'] },
+ dx: { value: 10, marks: ['text'] },
+ dy: { value: -10, marks: ['text'] },
+ align: { value: 'left', marks: ['text'] },
+ baseline: { value: 'top', marks: ['text'] },
+ fontSize: { value: 12, marks: ['text'] },
+ style: { value: 'arrow', marks: ['range'] },
+ colorByDirection: { value: true, marks: ['range'] },
+ units: { value: 100, marks: ['waffle'] },
+ columns: { value: 10, marks: ['waffle'] },
+ other: { value: 0.05, marks: ['waffle'] },
+ weekStart: { value: 'monday', marks: ['calendar'] },
+ cellRadius: { value: 2, marks: ['calendar'] },
+ shape: { value: 'hemicycle', marks: ['parliament'] },
+ seatRadius: { value: 3, marks: ['parliament'] },
+ majorityLine: { value: true, marks: ['parliament'] },
+ size: { value: 5, marks: MARK_CONTRACT_TYPES },
+ filled: { value: true, marks: MARK_CONTRACT_TYPES },
+ opacity: { value: 0.6, marks: MARK_CONTRACT_TYPES },
+ fillOpacity: { value: 0.5, marks: MARK_CONTRACT_TYPES },
+ fill: { value: '#ff0000', marks: MARK_CONTRACT_TYPES },
+ stroke: { value: '#333333', marks: MARK_CONTRACT_TYPES },
+ strokeWidth: { value: 2, marks: MARK_CONTRACT_TYPES },
+ tooltip: { value: true, marks: MARK_CONTRACT_TYPES },
+ clip: { value: true, marks: MARK_CONTRACT_TYPES },
+ fillPattern: { value: 'none', marks: MARK_CONTRACT_TYPES },
+} as const;
+
+type UnionKeys = T extends unknown ? keyof T : never;
+type AllowedMarks = T extends { type: infer M }
+ ? K extends keyof T
+ ? M
+ : never
+ : never;
+type Equal = [A] extends [B] ? ([B] extends [A] ? true : false) : false;
+type Assert = T;
+type Cases = typeof MARK_OPTION_CASES;
+/** These compile as part of engine typecheck, unlike Vitest-only type assertions. */
+export type MarkContractTypeChecks = [
+ Assert>,
+ Assert, 'type'>>>,
+ Assert<
+ Equal<
+ { [K in keyof Cases]: Equal> }[keyof Cases],
+ true
+ >
+ >,
+];
diff --git a/packages/engine/src/__test-fixtures__/specs.ts b/packages/engine/src/__test-fixtures__/specs.ts
index efe4c3cb..38a80377 100644
--- a/packages/engine/src/__test-fixtures__/specs.ts
+++ b/packages/engine/src/__test-fixtures__/specs.ts
@@ -61,6 +61,7 @@ export function makeLineSpec(): NormalizedChartSpec {
y: { field: 'value', type: 'quantitative' },
color: { field: 'country', type: 'nominal' },
},
+ chromeLayout: 'notes',
chrome: {
title: { text: 'GDP Growth' },
subtitle: { text: 'US vs UK over time' },
@@ -70,7 +71,6 @@ export function makeLineSpec(): NormalizedChartSpec {
responsive: true,
autoThin: true,
theme: {},
- darkMode: 'off',
labels: { density: 'auto', format: '', prefix: '' },
hiddenSeries: [],
seriesStyles: {},
@@ -109,12 +109,12 @@ export function makeBarSpec(): NormalizedChartSpec {
x: { field: 'value', type: 'quantitative' },
y: { field: 'name', type: 'nominal' },
},
+ chromeLayout: 'notes',
chrome: {},
annotations: [],
responsive: true,
autoThin: true,
theme: {},
- darkMode: 'off',
labels: { density: 'auto', format: '', prefix: '' },
hiddenSeries: [],
seriesStyles: {},
@@ -155,12 +155,12 @@ export function makeScatterSpec(): NormalizedChartSpec {
x: { field: 'x', type: 'quantitative' },
y: { field: 'y', type: 'quantitative' },
},
+ chromeLayout: 'notes',
chrome: {},
annotations: [],
responsive: true,
autoThin: true,
theme: {},
- darkMode: 'off',
labels: { density: 'auto', format: '', prefix: '' },
hiddenSeries: [],
seriesStyles: {},
diff --git a/packages/engine/src/__test-fixtures__/v8-styling.json b/packages/engine/src/__test-fixtures__/v8-styling.json
new file mode 100644
index 00000000..d5bbfdd5
--- /dev/null
+++ b/packages/engine/src/__test-fixtures__/v8-styling.json
@@ -0,0 +1,1552 @@
+[
+ {
+ "name": "defaults and annotations",
+ "source": {
+ "mark": "point",
+ "data": [
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 1,
+ "y": 1
+ }
+ ],
+ "encoding": {
+ "x": {
+ "field": "x",
+ "type": "quantitative",
+ "scale": {
+ "zero": false
+ }
+ },
+ "y": {
+ "field": "y",
+ "type": "quantitative",
+ "scale": {
+ "zero": false
+ }
+ }
+ },
+ "chrome": {
+ "title": "Title",
+ "source": "Source"
+ },
+ "annotations": [
+ {
+ "type": "text",
+ "x": 0,
+ "y": 0,
+ "text": "Headline",
+ "subtitle": "Detail",
+ "connector": "curve"
+ },
+ {
+ "type": "refline",
+ "y": 1,
+ "label": "Rule"
+ },
+ {
+ "type": "range",
+ "y1": 0,
+ "y2": 1,
+ "label": "Range"
+ }
+ ]
+ },
+ "expected": {
+ "fonts": {
+ "family": "\"Inter Variable\", Inter, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif",
+ "mono": "\"JetBrains Mono\", \"Fira Code\", \"Cascadia Code\", monospace",
+ "sizes": {
+ "title": 26,
+ "subtitle": 14,
+ "body": 13,
+ "small": 11,
+ "axisTick": 11,
+ "metricLabel": 11,
+ "metricValue": 22
+ },
+ "weights": {
+ "normal": 400,
+ "medium": 500,
+ "semibold": 600,
+ "bold": 700
+ }
+ },
+ "spacing": {
+ "padding": 20,
+ "chromeGap": 6,
+ "chromeToChart": 8,
+ "chartToFooter": 8,
+ "axisMargin": 6,
+ "xAxisHeight": 26,
+ "xAxisLabelPadding": 4
+ },
+ "chrome": {
+ "eyebrow": {
+ "fontSize": 11,
+ "fontWeight": 500,
+ "color": "#06b6d4",
+ "lineHeight": 1.4
+ },
+ "title": {
+ "fontSize": 26,
+ "fontWeight": 600,
+ "color": "#09090b",
+ "lineHeight": 1.15
+ },
+ "subtitle": {
+ "fontSize": 14,
+ "fontWeight": 400,
+ "color": "#71717a",
+ "lineHeight": 1.45
+ },
+ "source": {
+ "fontSize": 11,
+ "fontWeight": 400,
+ "color": "#71717a",
+ "lineHeight": 1.4
+ },
+ "byline": {
+ "fontSize": 11,
+ "fontWeight": 400,
+ "color": "#71717a",
+ "lineHeight": 1.4
+ },
+ "footer": {
+ "fontSize": 11,
+ "fontWeight": 400,
+ "color": "#71717a",
+ "lineHeight": 1.4
+ }
+ },
+ "colors": {
+ "categorical": ["#06b6d4", "#e29e47", "#4170cb", "#b2417f", "#3ca368", "#a584dc"],
+ "categoricalFill": ["#51bccc", "#dda96b", "#587fc8", "#bf6291", "#62ab7d", "#ae96da"],
+ "sequential": {
+ "blue": ["#deebf7", "#c6dbef", "#9ecae1", "#6baed6", "#3182bd", "#08519c"],
+ "green": ["#e5f5e0", "#c7e9c0", "#a1d99b", "#74c476", "#31a354", "#006d2c"],
+ "orange": ["#fee6ce", "#fdd0a2", "#fdae6b", "#fd8d3c", "#e6550d", "#a63603"],
+ "purple": ["#efedf5", "#dadaeb", "#bcbddc", "#9e9ac8", "#756bb1", "#54278f"],
+ "teal": ["#06b6d4", "#05a3be", "#0490a8", "#037d92", "#026a7c", "#015766", "#004450"]
+ },
+ "diverging": {
+ "redBlue": ["#b2182b", "#d6604d", "#f4a582", "#f7f7f7", "#92c5de", "#4393c3", "#2166ac"],
+ "brownTeal": ["#8c510a", "#bf812d", "#dfc27d", "#f6e8c3", "#80cdc1", "#35978f", "#01665e"]
+ },
+ "background": "transparent",
+ "text": "#09090b",
+ "gridline": "rgba(0,0,0,0.08)",
+ "hairline": "rgba(0,0,0,0.14)",
+ "axis": "#71717a",
+ "annotationFill": "rgba(0,0,0,0.04)",
+ "annotationText": "#71717a",
+ "annotationLabel": "#333333",
+ "annotationMuted": "#6b7280",
+ "annotationLeader": "#8f8f8f",
+ "annotationRule": "#666666",
+ "annotationRangeFill": "#000000",
+ "annotationMarkerFill": "#ffffff",
+ "annotationPlate": "#ffffff",
+ "positive": "#15803d",
+ "negative": "#dc2626",
+ "neutral": {
+ "100": "#f0f0f0",
+ "200": "#e1e1e2",
+ "300": "#c9c9c9",
+ "400": "#9d9d9d",
+ "600": "#787879",
+ "800": "#474748",
+ "secondary": "#3f3f46",
+ "faint": "#d4d4d8",
+ "border": "rgba(0, 0, 0, 0.08)",
+ "surface": "#ffffff"
+ }
+ },
+ "annotations": [
+ {
+ "type": "text",
+ "label": {
+ "text": "Headline",
+ "style": {
+ "fontFamily": "\"Inter Variable\", Inter, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif",
+ "fontSize": 13,
+ "fontWeight": 700,
+ "fill": "#333333",
+ "lineHeight": 1.3,
+ "textAnchor": "start"
+ }
+ },
+ "subtitle": {
+ "text": "Detail",
+ "style": {
+ "fontFamily": "\"Inter Variable\", Inter, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif",
+ "fontSize": 11,
+ "fontWeight": 400,
+ "fill": "#6b7280",
+ "lineHeight": 1.3,
+ "textAnchor": "start"
+ }
+ },
+ "connector": {
+ "arrow": true,
+ "stroke": "#333333"
+ }
+ },
+ {
+ "type": "refline",
+ "label": {
+ "text": "Rule",
+ "style": {
+ "fontFamily": "\"Inter Variable\", Inter, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif",
+ "fontSize": 11,
+ "fontWeight": 400,
+ "fill": "#666666",
+ "lineHeight": 1.3,
+ "textAnchor": "start"
+ }
+ }
+ },
+ {
+ "type": "range",
+ "label": {
+ "text": "Range",
+ "style": {
+ "fontFamily": "\"Inter Variable\", Inter, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif",
+ "fontSize": 11,
+ "fontWeight": 500,
+ "fill": "#333333",
+ "lineHeight": 1.3,
+ "textAnchor": "middle"
+ }
+ }
+ }
+ ]
+ }
+ },
+ {
+ "name": "authored typography 1",
+ "source": {
+ "mark": "point",
+ "data": [
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 1,
+ "y": 1
+ }
+ ],
+ "encoding": {
+ "x": {
+ "field": "x",
+ "type": "quantitative",
+ "scale": {
+ "zero": false
+ }
+ },
+ "y": {
+ "field": "y",
+ "type": "quantitative",
+ "scale": {
+ "zero": false
+ }
+ }
+ },
+ "theme": {
+ "chrome": {
+ "title": {
+ "fontSize": 31
+ },
+ "source": {
+ "fontSize": 16
+ }
+ }
+ },
+ "chrome": {
+ "title": "Title",
+ "source": "Source",
+ "footer": "Footer"
+ }
+ },
+ "expected": {
+ "fonts": {
+ "family": "\"Inter Variable\", Inter, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif",
+ "mono": "\"JetBrains Mono\", \"Fira Code\", \"Cascadia Code\", monospace",
+ "sizes": {
+ "title": 26,
+ "subtitle": 14,
+ "body": 13,
+ "small": 11,
+ "axisTick": 11,
+ "metricLabel": 11,
+ "metricValue": 22
+ },
+ "weights": {
+ "normal": 400,
+ "medium": 500,
+ "semibold": 600,
+ "bold": 700
+ }
+ },
+ "spacing": {
+ "padding": 20,
+ "chromeGap": 6,
+ "chromeToChart": 8,
+ "chartToFooter": 8,
+ "axisMargin": 6,
+ "xAxisHeight": 26,
+ "xAxisLabelPadding": 4
+ },
+ "chrome": {
+ "eyebrow": {
+ "fontSize": 11,
+ "fontWeight": 500,
+ "color": "#06b6d4",
+ "lineHeight": 1.4
+ },
+ "title": {
+ "fontSize": 31,
+ "fontWeight": 600,
+ "color": "#09090b",
+ "lineHeight": 1.15
+ },
+ "subtitle": {
+ "fontSize": 14,
+ "fontWeight": 400,
+ "color": "#71717a",
+ "lineHeight": 1.45
+ },
+ "source": {
+ "fontSize": 16,
+ "fontWeight": 400,
+ "color": "#71717a",
+ "lineHeight": 1.4
+ },
+ "byline": {
+ "fontSize": 11,
+ "fontWeight": 400,
+ "color": "#71717a",
+ "lineHeight": 1.4
+ },
+ "footer": {
+ "fontSize": 11,
+ "fontWeight": 400,
+ "color": "#71717a",
+ "lineHeight": 1.4
+ }
+ },
+ "colors": {
+ "categorical": ["#06b6d4", "#e29e47", "#4170cb", "#b2417f", "#3ca368", "#a584dc"],
+ "categoricalFill": ["#51bccc", "#dda96b", "#587fc8", "#bf6291", "#62ab7d", "#ae96da"],
+ "sequential": {
+ "blue": ["#deebf7", "#c6dbef", "#9ecae1", "#6baed6", "#3182bd", "#08519c"],
+ "green": ["#e5f5e0", "#c7e9c0", "#a1d99b", "#74c476", "#31a354", "#006d2c"],
+ "orange": ["#fee6ce", "#fdd0a2", "#fdae6b", "#fd8d3c", "#e6550d", "#a63603"],
+ "purple": ["#efedf5", "#dadaeb", "#bcbddc", "#9e9ac8", "#756bb1", "#54278f"],
+ "teal": ["#06b6d4", "#05a3be", "#0490a8", "#037d92", "#026a7c", "#015766", "#004450"]
+ },
+ "diverging": {
+ "redBlue": ["#b2182b", "#d6604d", "#f4a582", "#f7f7f7", "#92c5de", "#4393c3", "#2166ac"],
+ "brownTeal": ["#8c510a", "#bf812d", "#dfc27d", "#f6e8c3", "#80cdc1", "#35978f", "#01665e"]
+ },
+ "background": "transparent",
+ "text": "#09090b",
+ "gridline": "rgba(0,0,0,0.08)",
+ "hairline": "rgba(0,0,0,0.14)",
+ "axis": "#71717a",
+ "annotationFill": "rgba(0,0,0,0.04)",
+ "annotationText": "#71717a",
+ "annotationLabel": "#333333",
+ "annotationMuted": "#6b7280",
+ "annotationLeader": "#8f8f8f",
+ "annotationRule": "#666666",
+ "annotationRangeFill": "#000000",
+ "annotationMarkerFill": "#ffffff",
+ "annotationPlate": "#ffffff",
+ "positive": "#15803d",
+ "negative": "#dc2626",
+ "neutral": {
+ "100": "#f0f0f0",
+ "200": "#e1e1e2",
+ "300": "#c9c9c9",
+ "400": "#9d9d9d",
+ "600": "#787879",
+ "800": "#474748",
+ "secondary": "#3f3f46",
+ "faint": "#d4d4d8",
+ "border": "rgba(0, 0, 0, 0.08)",
+ "surface": "#ffffff"
+ }
+ },
+ "annotations": []
+ }
+ },
+ {
+ "name": "authored typography 2",
+ "source": {
+ "mark": "point",
+ "data": [
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 1,
+ "y": 1
+ }
+ ],
+ "encoding": {
+ "x": {
+ "field": "x",
+ "type": "quantitative",
+ "scale": {
+ "zero": false
+ }
+ },
+ "y": {
+ "field": "y",
+ "type": "quantitative",
+ "scale": {
+ "zero": false
+ }
+ }
+ },
+ "theme": {
+ "fonts": {
+ "sizes": {
+ "small": 15,
+ "title": 32
+ }
+ }
+ },
+ "chrome": {
+ "title": "Title",
+ "source": "Source",
+ "footer": "Footer"
+ }
+ },
+ "expected": {
+ "fonts": {
+ "family": "\"Inter Variable\", Inter, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif",
+ "mono": "\"JetBrains Mono\", \"Fira Code\", \"Cascadia Code\", monospace",
+ "sizes": {
+ "title": 32,
+ "subtitle": 14,
+ "body": 13,
+ "small": 15,
+ "axisTick": 11,
+ "metricLabel": 11,
+ "metricValue": 22
+ },
+ "weights": {
+ "normal": 400,
+ "medium": 500,
+ "semibold": 600,
+ "bold": 700
+ }
+ },
+ "spacing": {
+ "padding": 20,
+ "chromeGap": 6,
+ "chromeToChart": 8,
+ "chartToFooter": 8,
+ "axisMargin": 6,
+ "xAxisHeight": 26,
+ "xAxisLabelPadding": 4
+ },
+ "chrome": {
+ "eyebrow": {
+ "fontSize": 11,
+ "fontWeight": 500,
+ "color": "#06b6d4",
+ "lineHeight": 1.4
+ },
+ "title": {
+ "fontSize": 32,
+ "fontWeight": 600,
+ "color": "#09090b",
+ "lineHeight": 1.15
+ },
+ "subtitle": {
+ "fontSize": 14,
+ "fontWeight": 400,
+ "color": "#71717a",
+ "lineHeight": 1.45
+ },
+ "source": {
+ "fontSize": 15,
+ "fontWeight": 400,
+ "color": "#71717a",
+ "lineHeight": 1.4
+ },
+ "byline": {
+ "fontSize": 15,
+ "fontWeight": 400,
+ "color": "#71717a",
+ "lineHeight": 1.4
+ },
+ "footer": {
+ "fontSize": 15,
+ "fontWeight": 400,
+ "color": "#71717a",
+ "lineHeight": 1.4
+ }
+ },
+ "colors": {
+ "categorical": ["#06b6d4", "#e29e47", "#4170cb", "#b2417f", "#3ca368", "#a584dc"],
+ "categoricalFill": ["#51bccc", "#dda96b", "#587fc8", "#bf6291", "#62ab7d", "#ae96da"],
+ "sequential": {
+ "blue": ["#deebf7", "#c6dbef", "#9ecae1", "#6baed6", "#3182bd", "#08519c"],
+ "green": ["#e5f5e0", "#c7e9c0", "#a1d99b", "#74c476", "#31a354", "#006d2c"],
+ "orange": ["#fee6ce", "#fdd0a2", "#fdae6b", "#fd8d3c", "#e6550d", "#a63603"],
+ "purple": ["#efedf5", "#dadaeb", "#bcbddc", "#9e9ac8", "#756bb1", "#54278f"],
+ "teal": ["#06b6d4", "#05a3be", "#0490a8", "#037d92", "#026a7c", "#015766", "#004450"]
+ },
+ "diverging": {
+ "redBlue": ["#b2182b", "#d6604d", "#f4a582", "#f7f7f7", "#92c5de", "#4393c3", "#2166ac"],
+ "brownTeal": ["#8c510a", "#bf812d", "#dfc27d", "#f6e8c3", "#80cdc1", "#35978f", "#01665e"]
+ },
+ "background": "transparent",
+ "text": "#09090b",
+ "gridline": "rgba(0,0,0,0.08)",
+ "hairline": "rgba(0,0,0,0.14)",
+ "axis": "#71717a",
+ "annotationFill": "rgba(0,0,0,0.04)",
+ "annotationText": "#71717a",
+ "annotationLabel": "#333333",
+ "annotationMuted": "#6b7280",
+ "annotationLeader": "#8f8f8f",
+ "annotationRule": "#666666",
+ "annotationRangeFill": "#000000",
+ "annotationMarkerFill": "#ffffff",
+ "annotationPlate": "#ffffff",
+ "positive": "#15803d",
+ "negative": "#dc2626",
+ "neutral": {
+ "100": "#f0f0f0",
+ "200": "#e1e1e2",
+ "300": "#c9c9c9",
+ "400": "#9d9d9d",
+ "600": "#787879",
+ "800": "#474748",
+ "secondary": "#3f3f46",
+ "faint": "#d4d4d8",
+ "border": "rgba(0, 0, 0, 0.08)",
+ "surface": "#ffffff"
+ }
+ },
+ "annotations": []
+ }
+ },
+ {
+ "name": "authored typography 3",
+ "source": {
+ "mark": "point",
+ "data": [
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 1,
+ "y": 1
+ }
+ ],
+ "encoding": {
+ "x": {
+ "field": "x",
+ "type": "quantitative",
+ "scale": {
+ "zero": false
+ }
+ },
+ "y": {
+ "field": "y",
+ "type": "quantitative",
+ "scale": {
+ "zero": false
+ }
+ }
+ },
+ "theme": {
+ "fonts": {
+ "sizes": {
+ "body": 16
+ }
+ },
+ "chrome": {
+ "footer": {
+ "fontSize": 19
+ }
+ }
+ },
+ "chrome": {
+ "title": "Title",
+ "source": "Source",
+ "footer": "Footer"
+ }
+ },
+ "expected": {
+ "fonts": {
+ "family": "\"Inter Variable\", Inter, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif",
+ "mono": "\"JetBrains Mono\", \"Fira Code\", \"Cascadia Code\", monospace",
+ "sizes": {
+ "title": 26,
+ "subtitle": 14,
+ "body": 16,
+ "small": 11,
+ "axisTick": 11,
+ "metricLabel": 11,
+ "metricValue": 22
+ },
+ "weights": {
+ "normal": 400,
+ "medium": 500,
+ "semibold": 600,
+ "bold": 700
+ }
+ },
+ "spacing": {
+ "padding": 20,
+ "chromeGap": 6,
+ "chromeToChart": 8,
+ "chartToFooter": 8,
+ "axisMargin": 6,
+ "xAxisHeight": 26,
+ "xAxisLabelPadding": 4
+ },
+ "chrome": {
+ "eyebrow": {
+ "fontSize": 11,
+ "fontWeight": 500,
+ "color": "#06b6d4",
+ "lineHeight": 1.4
+ },
+ "title": {
+ "fontSize": 26,
+ "fontWeight": 600,
+ "color": "#09090b",
+ "lineHeight": 1.15
+ },
+ "subtitle": {
+ "fontSize": 14,
+ "fontWeight": 400,
+ "color": "#71717a",
+ "lineHeight": 1.45
+ },
+ "source": {
+ "fontSize": 11,
+ "fontWeight": 400,
+ "color": "#71717a",
+ "lineHeight": 1.4
+ },
+ "byline": {
+ "fontSize": 11,
+ "fontWeight": 400,
+ "color": "#71717a",
+ "lineHeight": 1.4
+ },
+ "footer": {
+ "fontSize": 19,
+ "fontWeight": 400,
+ "color": "#71717a",
+ "lineHeight": 1.4
+ }
+ },
+ "colors": {
+ "categorical": ["#06b6d4", "#e29e47", "#4170cb", "#b2417f", "#3ca368", "#a584dc"],
+ "categoricalFill": ["#51bccc", "#dda96b", "#587fc8", "#bf6291", "#62ab7d", "#ae96da"],
+ "sequential": {
+ "blue": ["#deebf7", "#c6dbef", "#9ecae1", "#6baed6", "#3182bd", "#08519c"],
+ "green": ["#e5f5e0", "#c7e9c0", "#a1d99b", "#74c476", "#31a354", "#006d2c"],
+ "orange": ["#fee6ce", "#fdd0a2", "#fdae6b", "#fd8d3c", "#e6550d", "#a63603"],
+ "purple": ["#efedf5", "#dadaeb", "#bcbddc", "#9e9ac8", "#756bb1", "#54278f"],
+ "teal": ["#06b6d4", "#05a3be", "#0490a8", "#037d92", "#026a7c", "#015766", "#004450"]
+ },
+ "diverging": {
+ "redBlue": ["#b2182b", "#d6604d", "#f4a582", "#f7f7f7", "#92c5de", "#4393c3", "#2166ac"],
+ "brownTeal": ["#8c510a", "#bf812d", "#dfc27d", "#f6e8c3", "#80cdc1", "#35978f", "#01665e"]
+ },
+ "background": "transparent",
+ "text": "#09090b",
+ "gridline": "rgba(0,0,0,0.08)",
+ "hairline": "rgba(0,0,0,0.14)",
+ "axis": "#71717a",
+ "annotationFill": "rgba(0,0,0,0.04)",
+ "annotationText": "#71717a",
+ "annotationLabel": "#333333",
+ "annotationMuted": "#6b7280",
+ "annotationLeader": "#8f8f8f",
+ "annotationRule": "#666666",
+ "annotationRangeFill": "#000000",
+ "annotationMarkerFill": "#ffffff",
+ "annotationPlate": "#ffffff",
+ "positive": "#15803d",
+ "negative": "#dc2626",
+ "neutral": {
+ "100": "#f0f0f0",
+ "200": "#e1e1e2",
+ "300": "#c9c9c9",
+ "400": "#9d9d9d",
+ "600": "#787879",
+ "800": "#474748",
+ "secondary": "#3f3f46",
+ "faint": "#d4d4d8",
+ "border": "rgba(0, 0, 0, 0.08)",
+ "surface": "#ffffff"
+ }
+ },
+ "annotations": []
+ }
+ },
+ {
+ "name": "partial child theme replaces parent",
+ "source": {
+ "theme": {
+ "colors": {
+ "categorical": ["green"]
+ }
+ },
+ "layer": [
+ {
+ "mark": "point",
+ "data": [
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 1,
+ "y": 1
+ }
+ ],
+ "encoding": {
+ "x": {
+ "field": "x",
+ "type": "quantitative",
+ "scale": {
+ "zero": false
+ }
+ },
+ "y": {
+ "field": "y",
+ "type": "quantitative",
+ "scale": {
+ "zero": false
+ }
+ }
+ },
+ "theme": {
+ "fonts": {
+ "sizes": {
+ "small": 16
+ }
+ }
+ }
+ },
+ {
+ "mark": "point",
+ "data": [
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 1,
+ "y": 1
+ }
+ ],
+ "encoding": {
+ "x": {
+ "field": "x",
+ "type": "quantitative",
+ "scale": {
+ "zero": false
+ }
+ },
+ "y": {
+ "field": "y",
+ "type": "quantitative",
+ "scale": {
+ "zero": false
+ }
+ }
+ }
+ }
+ ]
+ },
+ "expected": {
+ "fonts": {
+ "family": "\"Inter Variable\", Inter, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif",
+ "mono": "\"JetBrains Mono\", \"Fira Code\", \"Cascadia Code\", monospace",
+ "sizes": {
+ "title": 26,
+ "subtitle": 14,
+ "body": 13,
+ "small": 11,
+ "axisTick": 11,
+ "metricLabel": 11,
+ "metricValue": 22
+ },
+ "weights": {
+ "normal": 400,
+ "medium": 500,
+ "semibold": 600,
+ "bold": 700
+ }
+ },
+ "spacing": {
+ "padding": 20,
+ "chromeGap": 6,
+ "chromeToChart": 8,
+ "chartToFooter": 8,
+ "axisMargin": 6,
+ "xAxisHeight": 26,
+ "xAxisLabelPadding": 4
+ },
+ "chrome": {
+ "eyebrow": {
+ "fontSize": 11,
+ "fontWeight": 500,
+ "color": "#06b6d4",
+ "lineHeight": 1.4
+ },
+ "title": {
+ "fontSize": 26,
+ "fontWeight": 600,
+ "color": "#09090b",
+ "lineHeight": 1.15
+ },
+ "subtitle": {
+ "fontSize": 14,
+ "fontWeight": 400,
+ "color": "#71717a",
+ "lineHeight": 1.45
+ },
+ "source": {
+ "fontSize": 11,
+ "fontWeight": 400,
+ "color": "#71717a",
+ "lineHeight": 1.4
+ },
+ "byline": {
+ "fontSize": 11,
+ "fontWeight": 400,
+ "color": "#71717a",
+ "lineHeight": 1.4
+ },
+ "footer": {
+ "fontSize": 11,
+ "fontWeight": 400,
+ "color": "#71717a",
+ "lineHeight": 1.4
+ }
+ },
+ "colors": {
+ "categorical": ["green"],
+ "categoricalFill": ["green"],
+ "sequential": {
+ "blue": ["#deebf7", "#c6dbef", "#9ecae1", "#6baed6", "#3182bd", "#08519c"],
+ "green": ["#e5f5e0", "#c7e9c0", "#a1d99b", "#74c476", "#31a354", "#006d2c"],
+ "orange": ["#fee6ce", "#fdd0a2", "#fdae6b", "#fd8d3c", "#e6550d", "#a63603"],
+ "purple": ["#efedf5", "#dadaeb", "#bcbddc", "#9e9ac8", "#756bb1", "#54278f"],
+ "teal": ["#06b6d4", "#05a3be", "#0490a8", "#037d92", "#026a7c", "#015766", "#004450"]
+ },
+ "diverging": {
+ "redBlue": ["#b2182b", "#d6604d", "#f4a582", "#f7f7f7", "#92c5de", "#4393c3", "#2166ac"],
+ "brownTeal": ["#8c510a", "#bf812d", "#dfc27d", "#f6e8c3", "#80cdc1", "#35978f", "#01665e"]
+ },
+ "background": "transparent",
+ "text": "#09090b",
+ "gridline": "rgba(0,0,0,0.08)",
+ "hairline": "rgba(0,0,0,0.14)",
+ "axis": "#71717a",
+ "annotationFill": "rgba(0,0,0,0.04)",
+ "annotationText": "#71717a",
+ "annotationLabel": "#333333",
+ "annotationMuted": "#6b7280",
+ "annotationLeader": "#8f8f8f",
+ "annotationRule": "#666666",
+ "annotationRangeFill": "#000000",
+ "annotationMarkerFill": "#ffffff",
+ "annotationPlate": "#ffffff",
+ "positive": "#15803d",
+ "negative": "#dc2626",
+ "neutral": {
+ "100": "#f0f0f0",
+ "200": "#e1e1e2",
+ "300": "#c9c9c9",
+ "400": "#9d9d9d",
+ "600": "#787879",
+ "800": "#474748",
+ "secondary": "#3f3f46",
+ "faint": "#d4d4d8",
+ "border": "rgba(0, 0, 0, 0.08)",
+ "surface": "#ffffff"
+ }
+ },
+ "annotations": []
+ }
+ },
+ {
+ "name": "leaf owner nested=false",
+ "source": {
+ "layer": [
+ {
+ "mark": "point",
+ "data": [
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 1,
+ "y": 1
+ }
+ ],
+ "encoding": {
+ "x": {
+ "field": "x",
+ "type": "quantitative",
+ "scale": {
+ "zero": false
+ }
+ },
+ "y": {
+ "field": "y",
+ "type": "quantitative",
+ "scale": {
+ "zero": false
+ }
+ }
+ },
+ "responsive": false,
+ "theme": {
+ "colors": {
+ "background": "pink"
+ },
+ "fonts": {
+ "sizes": {
+ "title": 40
+ }
+ }
+ },
+ "chrome": {
+ "title": "Owned by first leaf"
+ },
+ "annotations": [
+ {
+ "type": "text",
+ "x": 0,
+ "y": 0,
+ "text": "Hidden"
+ }
+ ]
+ },
+ {
+ "mark": "point",
+ "data": [
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 1,
+ "y": 1
+ }
+ ],
+ "encoding": {
+ "x": {
+ "field": "x",
+ "type": "quantitative",
+ "scale": {
+ "zero": false
+ }
+ },
+ "y": {
+ "field": "y",
+ "type": "quantitative",
+ "scale": {
+ "zero": false
+ }
+ }
+ }
+ }
+ ]
+ },
+ "expected": {
+ "fonts": {
+ "family": "\"Inter Variable\", Inter, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif",
+ "mono": "\"JetBrains Mono\", \"Fira Code\", \"Cascadia Code\", monospace",
+ "sizes": {
+ "title": 40,
+ "subtitle": 14,
+ "body": 13,
+ "small": 11,
+ "axisTick": 11,
+ "metricLabel": 11,
+ "metricValue": 22
+ },
+ "weights": {
+ "normal": 400,
+ "medium": 500,
+ "semibold": 600,
+ "bold": 700
+ }
+ },
+ "spacing": {
+ "padding": 20,
+ "chromeGap": 6,
+ "chromeToChart": 8,
+ "chartToFooter": 8,
+ "axisMargin": 6,
+ "xAxisHeight": 26,
+ "xAxisLabelPadding": 4
+ },
+ "chrome": {
+ "eyebrow": {
+ "fontSize": 11,
+ "fontWeight": 500,
+ "color": "#06b6d4",
+ "lineHeight": 1.4
+ },
+ "title": {
+ "fontSize": 40,
+ "fontWeight": 600,
+ "color": "#09090b",
+ "lineHeight": 1.15
+ },
+ "subtitle": {
+ "fontSize": 14,
+ "fontWeight": 400,
+ "color": "#2d2d2e",
+ "lineHeight": 1.45
+ },
+ "source": {
+ "fontSize": 11,
+ "fontWeight": 400,
+ "color": "#454546",
+ "lineHeight": 1.4
+ },
+ "byline": {
+ "fontSize": 11,
+ "fontWeight": 400,
+ "color": "#454546",
+ "lineHeight": 1.4
+ },
+ "footer": {
+ "fontSize": 11,
+ "fontWeight": 400,
+ "color": "#454546",
+ "lineHeight": 1.4
+ }
+ },
+ "colors": {
+ "categorical": ["#06b6d4", "#e29e47", "#4170cb", "#b2417f", "#3ca368", "#a584dc"],
+ "categoricalFill": ["#51bccc", "#dda96b", "#587fc8", "#bf6291", "#62ab7d", "#ae96da"],
+ "sequential": {
+ "blue": ["#deebf7", "#c6dbef", "#9ecae1", "#6baed6", "#3182bd", "#08519c"],
+ "green": ["#e5f5e0", "#c7e9c0", "#a1d99b", "#74c476", "#31a354", "#006d2c"],
+ "orange": ["#fee6ce", "#fdd0a2", "#fdae6b", "#fd8d3c", "#e6550d", "#a63603"],
+ "purple": ["#efedf5", "#dadaeb", "#bcbddc", "#9e9ac8", "#756bb1", "#54278f"],
+ "teal": ["#06b6d4", "#05a3be", "#0490a8", "#037d92", "#026a7c", "#015766", "#004450"]
+ },
+ "diverging": {
+ "redBlue": ["#b2182b", "#d6604d", "#f4a582", "#f7f7f7", "#92c5de", "#4393c3", "#2166ac"],
+ "brownTeal": ["#8c510a", "#bf812d", "#dfc27d", "#f6e8c3", "#80cdc1", "#35978f", "#01665e"]
+ },
+ "background": "pink",
+ "text": "#09090b",
+ "gridline": "rgba(0,0,0,0.08)",
+ "hairline": "rgba(0,0,0,0.14)",
+ "axis": "#71717a",
+ "annotationFill": "rgba(0,0,0,0.04)",
+ "annotationText": "#71717a",
+ "annotationLabel": "#d1d5db",
+ "annotationMuted": "#9ca3af",
+ "annotationLeader": "#9aa0a6",
+ "annotationRule": "#666666",
+ "annotationRangeFill": "#000000",
+ "annotationMarkerFill": "#0a0a0a",
+ "annotationPlate": "#0a0a0a",
+ "positive": "#15803d",
+ "negative": "#dc2626",
+ "neutral": {
+ "100": "#171719",
+ "200": "#262627",
+ "300": "#3d3e3f",
+ "400": "#68696a",
+ "600": "#8c8c8d",
+ "800": "#bcbcbd",
+ "secondary": "#d0d6e0",
+ "faint": "#52525b",
+ "border": "rgba(255, 255, 255, 0.1)",
+ "surface": "pink"
+ }
+ },
+ "annotations": [
+ {
+ "type": "text",
+ "label": {
+ "text": "Hidden",
+ "style": {
+ "fontFamily": "\"Inter Variable\", Inter, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif",
+ "fontSize": 13,
+ "fontWeight": 400,
+ "fill": "#d1d5db",
+ "lineHeight": 1.3,
+ "textAnchor": "start"
+ }
+ },
+ "connector": {
+ "arrow": false,
+ "stroke": "#9aa0a6"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "name": "leaf owner nested=true",
+ "source": {
+ "layer": [
+ {
+ "layer": [
+ {
+ "mark": "point",
+ "data": [
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 1,
+ "y": 1
+ }
+ ],
+ "encoding": {
+ "x": {
+ "field": "x",
+ "type": "quantitative",
+ "scale": {
+ "zero": false
+ }
+ },
+ "y": {
+ "field": "y",
+ "type": "quantitative",
+ "scale": {
+ "zero": false
+ }
+ }
+ },
+ "responsive": false,
+ "theme": {
+ "colors": {
+ "background": "pink"
+ },
+ "fonts": {
+ "sizes": {
+ "title": 40
+ }
+ }
+ },
+ "chrome": {
+ "title": "Owned by first leaf"
+ },
+ "annotations": [
+ {
+ "type": "text",
+ "x": 0,
+ "y": 0,
+ "text": "Hidden"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "mark": "point",
+ "data": [
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 1,
+ "y": 1
+ }
+ ],
+ "encoding": {
+ "x": {
+ "field": "x",
+ "type": "quantitative",
+ "scale": {
+ "zero": false
+ }
+ },
+ "y": {
+ "field": "y",
+ "type": "quantitative",
+ "scale": {
+ "zero": false
+ }
+ }
+ }
+ }
+ ]
+ },
+ "expected": {
+ "fonts": {
+ "family": "\"Inter Variable\", Inter, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif",
+ "mono": "\"JetBrains Mono\", \"Fira Code\", \"Cascadia Code\", monospace",
+ "sizes": {
+ "title": 40,
+ "subtitle": 14,
+ "body": 13,
+ "small": 11,
+ "axisTick": 11,
+ "metricLabel": 11,
+ "metricValue": 22
+ },
+ "weights": {
+ "normal": 400,
+ "medium": 500,
+ "semibold": 600,
+ "bold": 700
+ }
+ },
+ "spacing": {
+ "padding": 20,
+ "chromeGap": 6,
+ "chromeToChart": 8,
+ "chartToFooter": 8,
+ "axisMargin": 6,
+ "xAxisHeight": 26,
+ "xAxisLabelPadding": 4
+ },
+ "chrome": {
+ "eyebrow": {
+ "fontSize": 11,
+ "fontWeight": 500,
+ "color": "#06b6d4",
+ "lineHeight": 1.4
+ },
+ "title": {
+ "fontSize": 40,
+ "fontWeight": 600,
+ "color": "#09090b",
+ "lineHeight": 1.15
+ },
+ "subtitle": {
+ "fontSize": 14,
+ "fontWeight": 400,
+ "color": "#2d2d2e",
+ "lineHeight": 1.45
+ },
+ "source": {
+ "fontSize": 11,
+ "fontWeight": 400,
+ "color": "#454546",
+ "lineHeight": 1.4
+ },
+ "byline": {
+ "fontSize": 11,
+ "fontWeight": 400,
+ "color": "#454546",
+ "lineHeight": 1.4
+ },
+ "footer": {
+ "fontSize": 11,
+ "fontWeight": 400,
+ "color": "#454546",
+ "lineHeight": 1.4
+ }
+ },
+ "colors": {
+ "categorical": ["#06b6d4", "#e29e47", "#4170cb", "#b2417f", "#3ca368", "#a584dc"],
+ "categoricalFill": ["#51bccc", "#dda96b", "#587fc8", "#bf6291", "#62ab7d", "#ae96da"],
+ "sequential": {
+ "blue": ["#deebf7", "#c6dbef", "#9ecae1", "#6baed6", "#3182bd", "#08519c"],
+ "green": ["#e5f5e0", "#c7e9c0", "#a1d99b", "#74c476", "#31a354", "#006d2c"],
+ "orange": ["#fee6ce", "#fdd0a2", "#fdae6b", "#fd8d3c", "#e6550d", "#a63603"],
+ "purple": ["#efedf5", "#dadaeb", "#bcbddc", "#9e9ac8", "#756bb1", "#54278f"],
+ "teal": ["#06b6d4", "#05a3be", "#0490a8", "#037d92", "#026a7c", "#015766", "#004450"]
+ },
+ "diverging": {
+ "redBlue": ["#b2182b", "#d6604d", "#f4a582", "#f7f7f7", "#92c5de", "#4393c3", "#2166ac"],
+ "brownTeal": ["#8c510a", "#bf812d", "#dfc27d", "#f6e8c3", "#80cdc1", "#35978f", "#01665e"]
+ },
+ "background": "pink",
+ "text": "#09090b",
+ "gridline": "rgba(0,0,0,0.08)",
+ "hairline": "rgba(0,0,0,0.14)",
+ "axis": "#71717a",
+ "annotationFill": "rgba(0,0,0,0.04)",
+ "annotationText": "#71717a",
+ "annotationLabel": "#d1d5db",
+ "annotationMuted": "#9ca3af",
+ "annotationLeader": "#9aa0a6",
+ "annotationRule": "#666666",
+ "annotationRangeFill": "#000000",
+ "annotationMarkerFill": "#0a0a0a",
+ "annotationPlate": "#0a0a0a",
+ "positive": "#15803d",
+ "negative": "#dc2626",
+ "neutral": {
+ "100": "#171719",
+ "200": "#262627",
+ "300": "#3d3e3f",
+ "400": "#68696a",
+ "600": "#8c8c8d",
+ "800": "#bcbcbd",
+ "secondary": "#d0d6e0",
+ "faint": "#52525b",
+ "border": "rgba(255, 255, 255, 0.1)",
+ "surface": "pink"
+ }
+ },
+ "annotations": [
+ {
+ "type": "text",
+ "label": {
+ "text": "Hidden",
+ "style": {
+ "fontFamily": "\"Inter Variable\", Inter, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif",
+ "fontSize": 13,
+ "fontWeight": 400,
+ "fill": "#d1d5db",
+ "lineHeight": 1.3,
+ "textAnchor": "start"
+ }
+ },
+ "connector": {
+ "arrow": false,
+ "stroke": "#9aa0a6"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "name": "leaf theme differs from parent",
+ "source": {
+ "theme": {
+ "colors": {
+ "categorical": ["#111111"]
+ }
+ },
+ "layer": [
+ {
+ "mark": "point",
+ "data": [
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 1,
+ "y": 1
+ }
+ ],
+ "encoding": {
+ "x": {
+ "field": "x",
+ "type": "quantitative",
+ "scale": {
+ "zero": false
+ }
+ },
+ "y": {
+ "field": "y",
+ "type": "quantitative",
+ "scale": {
+ "zero": false
+ }
+ }
+ }
+ },
+ {
+ "mark": "point",
+ "data": [
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 1,
+ "y": 1
+ }
+ ],
+ "encoding": {
+ "x": {
+ "field": "x",
+ "type": "quantitative",
+ "scale": {
+ "zero": false
+ }
+ },
+ "y": {
+ "field": "y",
+ "type": "quantitative",
+ "scale": {
+ "zero": false
+ }
+ }
+ },
+ "theme": {
+ "colors": {
+ "categorical": ["#ff0000"]
+ }
+ }
+ }
+ ]
+ },
+ "expected": {
+ "fonts": {
+ "family": "\"Inter Variable\", Inter, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif",
+ "mono": "\"JetBrains Mono\", \"Fira Code\", \"Cascadia Code\", monospace",
+ "sizes": {
+ "title": 26,
+ "subtitle": 14,
+ "body": 13,
+ "small": 11,
+ "axisTick": 11,
+ "metricLabel": 11,
+ "metricValue": 22
+ },
+ "weights": {
+ "normal": 400,
+ "medium": 500,
+ "semibold": 600,
+ "bold": 700
+ }
+ },
+ "spacing": {
+ "padding": 20,
+ "chromeGap": 6,
+ "chromeToChart": 8,
+ "chartToFooter": 8,
+ "axisMargin": 6,
+ "xAxisHeight": 26,
+ "xAxisLabelPadding": 4
+ },
+ "chrome": {
+ "eyebrow": {
+ "fontSize": 11,
+ "fontWeight": 500,
+ "color": "#06b6d4",
+ "lineHeight": 1.4
+ },
+ "title": {
+ "fontSize": 26,
+ "fontWeight": 600,
+ "color": "#09090b",
+ "lineHeight": 1.15
+ },
+ "subtitle": {
+ "fontSize": 14,
+ "fontWeight": 400,
+ "color": "#71717a",
+ "lineHeight": 1.45
+ },
+ "source": {
+ "fontSize": 11,
+ "fontWeight": 400,
+ "color": "#71717a",
+ "lineHeight": 1.4
+ },
+ "byline": {
+ "fontSize": 11,
+ "fontWeight": 400,
+ "color": "#71717a",
+ "lineHeight": 1.4
+ },
+ "footer": {
+ "fontSize": 11,
+ "fontWeight": 400,
+ "color": "#71717a",
+ "lineHeight": 1.4
+ }
+ },
+ "colors": {
+ "categorical": ["#111111"],
+ "categoricalFill": ["#111111"],
+ "sequential": {
+ "blue": ["#deebf7", "#c6dbef", "#9ecae1", "#6baed6", "#3182bd", "#08519c"],
+ "green": ["#e5f5e0", "#c7e9c0", "#a1d99b", "#74c476", "#31a354", "#006d2c"],
+ "orange": ["#fee6ce", "#fdd0a2", "#fdae6b", "#fd8d3c", "#e6550d", "#a63603"],
+ "purple": ["#efedf5", "#dadaeb", "#bcbddc", "#9e9ac8", "#756bb1", "#54278f"],
+ "teal": ["#06b6d4", "#05a3be", "#0490a8", "#037d92", "#026a7c", "#015766", "#004450"]
+ },
+ "diverging": {
+ "redBlue": ["#b2182b", "#d6604d", "#f4a582", "#f7f7f7", "#92c5de", "#4393c3", "#2166ac"],
+ "brownTeal": ["#8c510a", "#bf812d", "#dfc27d", "#f6e8c3", "#80cdc1", "#35978f", "#01665e"]
+ },
+ "background": "transparent",
+ "text": "#09090b",
+ "gridline": "rgba(0,0,0,0.08)",
+ "hairline": "rgba(0,0,0,0.14)",
+ "axis": "#71717a",
+ "annotationFill": "rgba(0,0,0,0.04)",
+ "annotationText": "#71717a",
+ "annotationLabel": "#333333",
+ "annotationMuted": "#6b7280",
+ "annotationLeader": "#8f8f8f",
+ "annotationRule": "#666666",
+ "annotationRangeFill": "#000000",
+ "annotationMarkerFill": "#ffffff",
+ "annotationPlate": "#ffffff",
+ "positive": "#15803d",
+ "negative": "#dc2626",
+ "neutral": {
+ "100": "#f0f0f0",
+ "200": "#e1e1e2",
+ "300": "#c9c9c9",
+ "400": "#9d9d9d",
+ "600": "#787879",
+ "800": "#474748",
+ "secondary": "#3f3f46",
+ "faint": "#d4d4d8",
+ "border": "rgba(0, 0, 0, 0.08)",
+ "surface": "#ffffff"
+ }
+ },
+ "annotations": [],
+ "marks": [
+ {
+ "type": "line",
+ "stroke": "#71717a"
+ },
+ {
+ "type": "point",
+ "fill": "#111111",
+ "stroke": "#ffffff"
+ },
+ {
+ "type": "point",
+ "fill": "#111111",
+ "stroke": "#ffffff"
+ },
+ {
+ "type": "line",
+ "stroke": "#71717a"
+ },
+ {
+ "type": "point",
+ "fill": "#ff0000",
+ "stroke": "#ffffff"
+ },
+ {
+ "type": "point",
+ "fill": "#ff0000",
+ "stroke": "#ffffff"
+ }
+ ]
+ }
+ }
+]
diff --git a/packages/engine/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap b/packages/engine/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap
index fe080369..960ca309 100644
--- a/packages/engine/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap
+++ b/packages/engine/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap
@@ -1,5 +1,1391 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+exports[`compileChart snapshot (Step 7 oracle) > annotated scatter with auto, pinned, reference and range annotations 1`] = `
+{
+ "a11y": {
+ "altText": "Scatter plot showing Annotated scatter (12 data points)",
+ "dataTableFallback": [
+ [
+ "x",
+ "y",
+ ],
+ [
+ 0,
+ 0,
+ ],
+ [
+ 10,
+ 37,
+ ],
+ [
+ 20,
+ 74,
+ ],
+ [
+ 30,
+ 11,
+ ],
+ [
+ 40,
+ 48,
+ ],
+ [
+ 50,
+ 85,
+ ],
+ [
+ 60,
+ 22,
+ ],
+ [
+ 70,
+ 59,
+ ],
+ [
+ 80,
+ 96,
+ ],
+ [
+ 90,
+ 33,
+ ],
+ [
+ 100,
+ 70,
+ ],
+ [
+ 110,
+ 7,
+ ],
+ ],
+ "keyboardNavigable": true,
+ "role": "img",
+ },
+ "animation": undefined,
+ "annotations": [
+ {
+ "bounds": {
+ "height": 16.9,
+ "width": 98.98200000000001,
+ "x": 203.7197171459506,
+ "y": 310.7560101267767,
+ },
+ "dot": {
+ "fill": "#ffffff",
+ "radius": 4,
+ "stroke": "#8f8f8f",
+ "strokeWidth": 1.5,
+ "x": 183.92072727272728,
+ "y": 347.455,
+ },
+ "fill": undefined,
+ "id": "auto",
+ "label": {
+ "background": undefined,
+ "bounds": {
+ "height": 16.9,
+ "width": 98.98200000000001,
+ "x": 203.7197171459506,
+ "y": 310.7560101267767,
+ },
+ "connector": {
+ "arrow": false,
+ "endpoint": {
+ "x": 183.92072727272728,
+ "y": 347.455,
+ },
+ "exit": "vertical",
+ "from": {
+ "x": 222.02263515688892,
+ "y": 331.9211485455619,
+ },
+ "fromOffset": undefined,
+ "stroke": "#8f8f8f",
+ "style": "straight",
+ "to": {
+ "x": 191.0972286610762,
+ "y": 344.5291962090691,
+ },
+ },
+ "halo": undefined,
+ "lines": [
+ "Automatic callout",
+ ],
+ "style": {
+ "fill": "#333333",
+ "fontFamily": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
+ "fontSize": 13,
+ "fontWeight": 400,
+ "lineHeight": 1.3,
+ "textAnchor": "start",
+ },
+ "text": "Automatic callout",
+ "visible": true,
+ "x": 203.7197171459506,
+ "y": 323.7560101267767,
+ },
+ "opacity": 1,
+ "specIndex": 0,
+ "stroke": undefined,
+ "subtitle": undefined,
+ "type": "text",
+ "zIndex": undefined,
+ },
+ {
+ "bounds": {
+ "height": 35.5,
+ "width": 81.65300000000002,
+ "x": 334.46036363636364,
+ "y": 187.89500000000004,
+ },
+ "dot": {
+ "fill": "#ffffff",
+ "radius": 4,
+ "stroke": "#8f8f8f",
+ "strokeWidth": 1.5,
+ "x": 366.46036363636364,
+ "y": 222.89500000000004,
+ },
+ "fill": undefined,
+ "id": "pinned",
+ "label": {
+ "background": undefined,
+ "bounds": {
+ "height": 16.9,
+ "width": 81.65300000000003,
+ "x": 334.46036363636364,
+ "y": 187.89500000000004,
+ },
+ "connector": undefined,
+ "halo": undefined,
+ "lines": [
+ "Pinned callout",
+ ],
+ "style": {
+ "fill": "#333333",
+ "fontFamily": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
+ "fontSize": 13,
+ "fontWeight": 400,
+ "lineHeight": 1.3,
+ "textAnchor": "start",
+ },
+ "text": "Pinned callout",
+ "visible": true,
+ "x": 334.46036363636364,
+ "y": 200.89500000000004,
+ },
+ "opacity": 1,
+ "specIndex": 1,
+ "stroke": undefined,
+ "subtitle": {
+ "lines": [
+ "with a subtitle",
+ ],
+ "style": {
+ "fill": "#6b7280",
+ "fontFamily": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
+ "fontSize": 12,
+ "fontWeight": 400,
+ "lineHeight": 1.3,
+ "textAnchor": "start",
+ },
+ "text": "with a subtitle",
+ "x": 334.46036363636364,
+ "y": 219.79500000000004,
+ },
+ "type": "text",
+ "zIndex": undefined,
+ },
+ {
+ "bounds": {
+ "height": 15.6,
+ "width": 34.68,
+ "x": 588,
+ "y": 238.45,
+ },
+ "id": "target",
+ "label": {
+ "background": "#ffffff",
+ "bounds": {
+ "height": 15.6,
+ "width": 34.68,
+ "x": 588,
+ "y": 238.45,
+ },
+ "connector": {
+ "arrow": false,
+ "endpoint": {
+ "x": 549,
+ "y": 246.25,
+ },
+ "exit": "horizontal",
+ "from": {
+ "x": 580,
+ "y": 246.25,
+ },
+ "labelAttachment": "left-center",
+ "stroke": "#888888",
+ "style": "straight",
+ "to": {
+ "x": 553,
+ "y": 246.25,
+ },
+ },
+ "lines": [
+ "Target",
+ ],
+ "style": {
+ "fill": "#888888",
+ "fontFamily": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
+ "fontSize": 12,
+ "fontWeight": 400,
+ "lineHeight": 1.3,
+ "textAnchor": "start",
+ },
+ "text": "Target",
+ "visible": true,
+ "x": 588,
+ "y": 250.45,
+ },
+ "line": {
+ "end": {
+ "x": 549,
+ "y": 246.25,
+ },
+ "start": {
+ "x": 47.016000000000005,
+ "y": 246.25,
+ },
+ },
+ "specIndex": 2,
+ "stroke": "#888888",
+ "strokeDasharray": "4 3",
+ "strokeWidth": 1,
+ "type": "refline",
+ "zIndex": undefined,
+ },
+ {
+ "bounds": {
+ "height": 15.6,
+ "width": 74.69999999999999,
+ "x": 192.2056363636364,
+ "y": 68.9,
+ },
+ "id": "event",
+ "label": {
+ "background": "#ffffff",
+ "bounds": {
+ "height": 15.6,
+ "width": 74.69999999999999,
+ "x": 192.2056363636364,
+ "y": 68.9,
+ },
+ "connector": {
+ "arrow": false,
+ "endpoint": {
+ "x": 229.55563636363638,
+ "y": 116.5,
+ },
+ "exit": "vertical",
+ "from": {
+ "x": 229.55563636363638,
+ "y": 92.5,
+ },
+ "stroke": "#888888",
+ "style": "straight",
+ "to": {
+ "x": 229.55563636363638,
+ "y": 112.5,
+ },
+ },
+ "lines": [
+ "Policy change",
+ ],
+ "style": {
+ "fill": "#888888",
+ "fontFamily": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
+ "fontSize": 12,
+ "fontWeight": 400,
+ "lineHeight": 1.3,
+ "textAnchor": "start",
+ },
+ "text": "Policy change",
+ "visible": true,
+ "x": 192.2056363636364,
+ "y": 80.9,
+ },
+ "line": {
+ "end": {
+ "x": 229.55563636363638,
+ "y": 376,
+ },
+ "start": {
+ "x": 229.55563636363638,
+ "y": 116.5,
+ },
+ },
+ "specIndex": 3,
+ "stroke": "#888888",
+ "strokeDasharray": "4 3",
+ "strokeWidth": 1,
+ "type": "refline",
+ "zIndex": undefined,
+ },
+ {
+ "bounds": {
+ "height": 15.6,
+ "width": 47.6136,
+ "x": 433.9233818181818,
+ "y": 118.5,
+ },
+ "fill": "#f0c040",
+ "id": "band",
+ "label": {
+ "style": {
+ "fill": "#333333",
+ "fontFamily": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
+ "fontSize": 12,
+ "fontWeight": 500,
+ "lineHeight": 1.3,
+ "textAnchor": "middle",
+ },
+ "text": "Forecast",
+ "visible": true,
+ "x": 457.7301818181818,
+ "y": 130.5,
+ },
+ "opacity": 0.1,
+ "rect": {
+ "height": 259.5,
+ "width": 91.26981818181815,
+ "x": 412.09527272727274,
+ "y": 116.5,
+ },
+ "specIndex": 4,
+ "stroke": undefined,
+ "type": "range",
+ "zIndex": undefined,
+ },
+ ],
+ "area": {
+ "height": 259.5,
+ "width": 501.98400000000004,
+ "x": 47.016000000000005,
+ "y": 116.5,
+ },
+ "axes": {
+ "x": {
+ "domainLine": false,
+ "end": {
+ "x": 549,
+ "y": 376,
+ },
+ "extent": 27,
+ "gridlines": [
+ {
+ "color": "rgba(0,0,0,0.22)",
+ "major": true,
+ "position": 47.016000000000005,
+ },
+ {
+ "major": true,
+ "position": 275.19054545454543,
+ },
+ {
+ "major": true,
+ "position": 503.3650909090909,
+ },
+ ],
+ "label": undefined,
+ "labelFlush": undefined,
+ "labelOverlap": undefined,
+ "labelPadding": undefined,
+ "labelStyle": {
+ "fill": "#09090b",
+ "fontFamily": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
+ "fontSize": 13,
+ "fontWeight": 500,
+ "lineHeight": 1.3,
+ },
+ "offset": undefined,
+ "orient": undefined,
+ "start": {
+ "x": 47.016000000000005,
+ "y": 376,
+ },
+ "tickAngle": undefined,
+ "tickLabelStyle": {
+ "fill": "#71717a",
+ "fontFamily": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
+ "fontSize": 12,
+ "fontVariant": "tabular-nums",
+ "fontWeight": 400,
+ "lineHeight": 1.2,
+ },
+ "tickMarks": undefined,
+ "tickPosition": "gutter",
+ "ticks": [
+ {
+ "label": "0",
+ "position": 47.016000000000005,
+ "value": 0,
+ },
+ {
+ "label": "50",
+ "position": 275.19054545454543,
+ "value": 50,
+ },
+ {
+ "label": "100",
+ "position": 503.3650909090909,
+ "value": 100,
+ },
+ ],
+ "titlePadding": undefined,
+ },
+ "y": {
+ "domainLine": undefined,
+ "end": {
+ "x": 47.016000000000005,
+ "y": 376,
+ },
+ "gridlines": [
+ {
+ "color": "rgba(0,0,0,0.22)",
+ "major": true,
+ "position": 376,
+ },
+ {
+ "major": true,
+ "position": 324.1,
+ },
+ {
+ "major": true,
+ "position": 272.2,
+ },
+ {
+ "major": true,
+ "position": 220.3,
+ },
+ {
+ "major": true,
+ "position": 168.39999999999998,
+ },
+ {
+ "major": true,
+ "position": 116.5,
+ },
+ ],
+ "label": undefined,
+ "labelFlush": undefined,
+ "labelOverlap": undefined,
+ "labelPadding": undefined,
+ "labelStyle": {
+ "fill": "#09090b",
+ "fontFamily": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
+ "fontSize": 13,
+ "fontWeight": 500,
+ "lineHeight": 1.3,
+ },
+ "offset": undefined,
+ "orient": undefined,
+ "start": {
+ "x": 47.016000000000005,
+ "y": 116.5,
+ },
+ "tickAngle": undefined,
+ "tickLabelStyle": {
+ "fill": "#71717a",
+ "fontFamily": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
+ "fontSize": 12,
+ "fontVariant": "tabular-nums",
+ "fontWeight": 400,
+ "lineHeight": 1.2,
+ },
+ "tickMarks": undefined,
+ "tickPosition": "gutter",
+ "ticks": [
+ {
+ "label": "0",
+ "position": 376,
+ "value": 0,
+ },
+ {
+ "label": "20",
+ "position": 324.1,
+ "value": 20,
+ },
+ {
+ "label": "40",
+ "position": 272.2,
+ "value": 40,
+ },
+ {
+ "label": "60",
+ "position": 220.3,
+ "value": 60,
+ },
+ {
+ "label": "80",
+ "position": 168.39999999999998,
+ "value": 80,
+ },
+ {
+ "label": "100",
+ "position": 116.5,
+ "value": 100,
+ },
+ ],
+ "titlePadding": undefined,
+ },
+ },
+ "chrome": {
+ "bottomAnchorY": 403,
+ "bottomHeight": 0,
+ "grownHeight": 50.80000000000001,
+ "noteBandHeight": 50.800000000000004,
+ "title": {
+ "maxWidth": 601,
+ "style": {
+ "fill": "#09090b",
+ "fontFamily": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
+ "fontSize": 26,
+ "fontWeight": 600,
+ "lineHeight": 1.15,
+ "textAnchor": "start",
+ },
+ "text": "Annotated scatter",
+ "x": 17,
+ "y": 17,
+ },
+ "topHeight": 45.9,
+ },
+ "crosshair": false,
+ "dimensions": {
+ "height": 470.8,
+ "width": 640,
+ },
+ "display": "full",
+ "legend": {
+ "bounds": {
+ "height": 0,
+ "width": 0,
+ "x": 0,
+ "y": 0,
+ },
+ "entries": [],
+ "entryGap": 16,
+ "labelStyle": {
+ "fill": "#09090b",
+ "fontFamily": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
+ "fontSize": 12,
+ "fontWeight": 400,
+ "lineHeight": 1.3,
+ },
+ "position": "top",
+ "swatchChipFill": "rgba(0,0,0,0.04)",
+ "swatchGap": 6,
+ "swatchSize": 12,
+ "type": "categorical",
+ },
+ "legends": [
+ {
+ "bounds": {
+ "height": 0,
+ "width": 0,
+ "x": 0,
+ "y": 0,
+ },
+ "entries": [],
+ "entryGap": 16,
+ "labelStyle": {
+ "fill": "#09090b",
+ "fontFamily": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
+ "fontSize": 12,
+ "fontWeight": 400,
+ "lineHeight": 1.3,
+ },
+ "position": "top",
+ "swatchChipFill": "rgba(0,0,0,0.04)",
+ "swatchGap": 6,
+ "swatchSize": 12,
+ "type": "categorical",
+ },
+ ],
+ "marks": [
+ {
+ "aria": {
+ "label": "Trend line: linear regression",
+ },
+ "data": [],
+ "points": [
+ {
+ "x": 47.016000000000005,
+ "y": 282.846153846154,
+ },
+ {
+ "x": 549,
+ "y": 234.73884615384605,
+ },
+ ],
+ "stroke": "#71717a",
+ "strokeDasharray": "4 4",
+ "strokeWidth": 1.5,
+ "type": "line",
+ },
+ {
+ "aria": {
+ "label": "Data point: x=0, y=0",
+ },
+ "cx": 47.016000000000005,
+ "cy": 376,
+ "data": {
+ "x": 0,
+ "y": 0,
+ },
+ "fill": "#06b6d4",
+ "fillOpacity": 0.85,
+ "key": "0",
+ "r": 5,
+ "stroke": "#ffffff",
+ "strokeWidth": 1.5,
+ "type": "point",
+ },
+ {
+ "aria": {
+ "label": "Data point: x=10, y=37",
+ },
+ "cx": 92.6509090909091,
+ "cy": 279.985,
+ "data": {
+ "x": 10,
+ "y": 37,
+ },
+ "fill": "#06b6d4",
+ "fillOpacity": 0.85,
+ "key": "10",
+ "r": 5,
+ "stroke": "#ffffff",
+ "strokeWidth": 1.5,
+ "type": "point",
+ },
+ {
+ "aria": {
+ "label": "Data point: x=20, y=74",
+ },
+ "cx": 138.2858181818182,
+ "cy": 183.97,
+ "data": {
+ "x": 20,
+ "y": 74,
+ },
+ "fill": "#06b6d4",
+ "fillOpacity": 0.85,
+ "key": "20",
+ "r": 5,
+ "stroke": "#ffffff",
+ "strokeWidth": 1.5,
+ "type": "point",
+ },
+ {
+ "aria": {
+ "label": "Data point: x=30, y=11",
+ },
+ "cx": 183.92072727272728,
+ "cy": 347.455,
+ "data": {
+ "x": 30,
+ "y": 11,
+ },
+ "fill": "#06b6d4",
+ "fillOpacity": 0.85,
+ "key": "30",
+ "r": 5,
+ "stroke": "#ffffff",
+ "strokeWidth": 1.5,
+ "type": "point",
+ },
+ {
+ "aria": {
+ "label": "Data point: x=40, y=48",
+ },
+ "cx": 229.55563636363638,
+ "cy": 251.44,
+ "data": {
+ "x": 40,
+ "y": 48,
+ },
+ "fill": "#06b6d4",
+ "fillOpacity": 0.85,
+ "key": "40",
+ "r": 5,
+ "stroke": "#ffffff",
+ "strokeWidth": 1.5,
+ "type": "point",
+ },
+ {
+ "aria": {
+ "label": "Data point: x=50, y=85",
+ },
+ "cx": 275.19054545454543,
+ "cy": 155.425,
+ "data": {
+ "x": 50,
+ "y": 85,
+ },
+ "fill": "#06b6d4",
+ "fillOpacity": 0.85,
+ "key": "50",
+ "r": 5,
+ "stroke": "#ffffff",
+ "strokeWidth": 1.5,
+ "type": "point",
+ },
+ {
+ "aria": {
+ "label": "Data point: x=60, y=22",
+ },
+ "cx": 320.82545454545453,
+ "cy": 318.91,
+ "data": {
+ "x": 60,
+ "y": 22,
+ },
+ "fill": "#06b6d4",
+ "fillOpacity": 0.85,
+ "key": "60",
+ "r": 5,
+ "stroke": "#ffffff",
+ "strokeWidth": 1.5,
+ "type": "point",
+ },
+ {
+ "aria": {
+ "label": "Data point: x=70, y=59",
+ },
+ "cx": 366.46036363636364,
+ "cy": 222.89500000000004,
+ "data": {
+ "x": 70,
+ "y": 59,
+ },
+ "fill": "#06b6d4",
+ "fillOpacity": 0.85,
+ "key": "70",
+ "r": 5,
+ "stroke": "#ffffff",
+ "strokeWidth": 1.5,
+ "type": "point",
+ },
+ {
+ "aria": {
+ "label": "Data point: x=80, y=96",
+ },
+ "cx": 412.09527272727274,
+ "cy": 126.88,
+ "data": {
+ "x": 80,
+ "y": 96,
+ },
+ "fill": "#06b6d4",
+ "fillOpacity": 0.85,
+ "key": "80",
+ "r": 5,
+ "stroke": "#ffffff",
+ "strokeWidth": 1.5,
+ "type": "point",
+ },
+ {
+ "aria": {
+ "label": "Data point: x=90, y=33",
+ },
+ "cx": 457.73018181818185,
+ "cy": 290.36499999999995,
+ "data": {
+ "x": 90,
+ "y": 33,
+ },
+ "fill": "#06b6d4",
+ "fillOpacity": 0.85,
+ "key": "90",
+ "r": 5,
+ "stroke": "#ffffff",
+ "strokeWidth": 1.5,
+ "type": "point",
+ },
+ {
+ "aria": {
+ "label": "Data point: x=100, y=70",
+ },
+ "cx": 503.3650909090909,
+ "cy": 194.35000000000002,
+ "data": {
+ "x": 100,
+ "y": 70,
+ },
+ "fill": "#06b6d4",
+ "fillOpacity": 0.85,
+ "key": "100",
+ "r": 5,
+ "stroke": "#ffffff",
+ "strokeWidth": 1.5,
+ "type": "point",
+ },
+ {
+ "aria": {
+ "label": "Data point: x=110, y=7",
+ },
+ "cx": 549,
+ "cy": 357.8349999999999,
+ "data": {
+ "x": 110,
+ "y": 7,
+ },
+ "fill": "#06b6d4",
+ "fillOpacity": 0.85,
+ "key": "110",
+ "r": 5,
+ "stroke": "#ffffff",
+ "strokeWidth": 1.5,
+ "type": "point",
+ },
+ ],
+ "metrics": undefined,
+ "theme": {
+ "_authored": {},
+ "_tokenPairs": undefined,
+ "borderRadius": 8,
+ "chrome": {
+ "byline": {
+ "color": "#71717a",
+ "fontSize": 12,
+ "fontWeight": 400,
+ "lineHeight": 1.4,
+ },
+ "eyebrow": {
+ "color": "#06b6d4",
+ "fontSize": 11,
+ "fontWeight": 500,
+ "lineHeight": 1.4,
+ },
+ "footer": {
+ "color": "#71717a",
+ "fontSize": 12,
+ "fontWeight": 400,
+ "lineHeight": 1.4,
+ },
+ "source": {
+ "color": "#71717a",
+ "fontSize": 12,
+ "fontWeight": 400,
+ "lineHeight": 1.4,
+ },
+ "subtitle": {
+ "color": "#71717a",
+ "fontSize": 14,
+ "fontWeight": 400,
+ "lineHeight": 1.45,
+ },
+ "title": {
+ "color": "#09090b",
+ "fontSize": 26,
+ "fontWeight": 600,
+ "lineHeight": 1.15,
+ },
+ },
+ "colors": {
+ "annotationFill": "rgba(0,0,0,0.04)",
+ "annotationLabel": "#333333",
+ "annotationLeader": "#8f8f8f",
+ "annotationMarkerFill": "#ffffff",
+ "annotationMuted": "#6b7280",
+ "annotationPlate": "#ffffff",
+ "annotationRangeFill": "#f0c040",
+ "annotationRule": "#888888",
+ "annotationText": "#71717a",
+ "axis": "#71717a",
+ "background": "transparent",
+ "categorical": [
+ "#06b6d4",
+ "#e29e47",
+ "#4170cb",
+ "#b2417f",
+ "#3ca368",
+ "#a584dc",
+ ],
+ "categoricalFill": [
+ "#51bccc",
+ "#dda96b",
+ "#587fc8",
+ "#bf6291",
+ "#62ab7d",
+ "#ae96da",
+ ],
+ "diverging": {
+ "brownTeal": [
+ "#8c510a",
+ "#bf812d",
+ "#dfc27d",
+ "#f6e8c3",
+ "#80cdc1",
+ "#35978f",
+ "#01665e",
+ ],
+ "redBlue": [
+ "#b2182b",
+ "#d6604d",
+ "#f4a582",
+ "#f7f7f7",
+ "#92c5de",
+ "#4393c3",
+ "#2166ac",
+ ],
+ },
+ "gridline": "rgba(0,0,0,0.05)",
+ "gridlineZero": "rgba(0,0,0,0.22)",
+ "hairline": "rgba(0,0,0,0.14)",
+ "mapInteriorBorder": "rgba(0,0,0,0.14)",
+ "mapOutlineBorder": "rgba(0,0,0,0.40)",
+ "negative": "#dc2626",
+ "neutral": {
+ "100": "#f0f0f0",
+ "200": "#e1e1e2",
+ "300": "#c9c9c9",
+ "400": "#9d9d9d",
+ "600": "#787879",
+ "800": "#474748",
+ "border": "rgba(0, 0, 0, 0.08)",
+ "faint": "#d4d4d8",
+ "secondary": "#3f3f46",
+ "surface": "#ffffff",
+ },
+ "positive": "#15803d",
+ "sequential": {
+ "blue": [
+ "#deebf7",
+ "#c6dbef",
+ "#9ecae1",
+ "#6baed6",
+ "#3182bd",
+ "#08519c",
+ ],
+ "green": [
+ "#e5f5e0",
+ "#c7e9c0",
+ "#a1d99b",
+ "#74c476",
+ "#31a354",
+ "#006d2c",
+ ],
+ "orange": [
+ "#fee6ce",
+ "#fdd0a2",
+ "#fdae6b",
+ "#fd8d3c",
+ "#e6550d",
+ "#a63603",
+ ],
+ "purple": [
+ "#efedf5",
+ "#dadaeb",
+ "#bcbddc",
+ "#9e9ac8",
+ "#756bb1",
+ "#54278f",
+ ],
+ "teal": [
+ "#06b6d4",
+ "#05a3be",
+ "#0490a8",
+ "#037d92",
+ "#026a7c",
+ "#015766",
+ "#004450",
+ ],
+ },
+ "text": "#09090b",
+ },
+ "fonts": {
+ "family": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
+ "mono": ""JetBrains Mono", "Fira Code", "Cascadia Code", monospace",
+ "sizes": {
+ "axisTick": 12,
+ "body": 13,
+ "endpointLabel": 13,
+ "endpointValue": 12,
+ "metricLabel": 11,
+ "metricValue": 22,
+ "small": 12,
+ "subtitle": 14,
+ "title": 26,
+ },
+ "weights": {
+ "bold": 700,
+ "medium": 500,
+ "normal": 400,
+ "semibold": 600,
+ },
+ },
+ "isDark": false,
+ "rule": null,
+ "seriesStrategy": "palette",
+ "spacing": {
+ "axisMargin": 6,
+ "chartToFooter": 12,
+ "chromeGap": 6,
+ "chromeToChart": 16,
+ "endpointColumnGap": 18,
+ "endpointEntryGap": 6,
+ "endpointValueGap": 4,
+ "padding": 20,
+ "xAxisHeight": 26,
+ "xAxisLabelPadding": 4,
+ },
+ },
+ "tooltipDescriptors": [
+ [
+ "point-1",
+ {
+ "fields": [
+ {
+ "color": "#06b6d4",
+ "label": "y",
+ "role": "value",
+ "source": {
+ "channel": "y",
+ "field": "y",
+ },
+ "value": "0",
+ },
+ {
+ "label": "x",
+ "role": "x",
+ "source": {
+ "channel": "x",
+ "field": "x",
+ },
+ "value": "0",
+ },
+ ],
+ "title": undefined,
+ },
+ ],
+ [
+ "point-2",
+ {
+ "fields": [
+ {
+ "color": "#06b6d4",
+ "label": "y",
+ "role": "value",
+ "source": {
+ "channel": "y",
+ "field": "y",
+ },
+ "value": "37",
+ },
+ {
+ "label": "x",
+ "role": "x",
+ "source": {
+ "channel": "x",
+ "field": "x",
+ },
+ "value": "10",
+ },
+ ],
+ "title": undefined,
+ },
+ ],
+ [
+ "point-3",
+ {
+ "fields": [
+ {
+ "color": "#06b6d4",
+ "label": "y",
+ "role": "value",
+ "source": {
+ "channel": "y",
+ "field": "y",
+ },
+ "value": "74",
+ },
+ {
+ "label": "x",
+ "role": "x",
+ "source": {
+ "channel": "x",
+ "field": "x",
+ },
+ "value": "20",
+ },
+ ],
+ "title": undefined,
+ },
+ ],
+ [
+ "point-4",
+ {
+ "fields": [
+ {
+ "color": "#06b6d4",
+ "label": "y",
+ "role": "value",
+ "source": {
+ "channel": "y",
+ "field": "y",
+ },
+ "value": "11",
+ },
+ {
+ "label": "x",
+ "role": "x",
+ "source": {
+ "channel": "x",
+ "field": "x",
+ },
+ "value": "30",
+ },
+ ],
+ "title": undefined,
+ },
+ ],
+ [
+ "point-5",
+ {
+ "fields": [
+ {
+ "color": "#06b6d4",
+ "label": "y",
+ "role": "value",
+ "source": {
+ "channel": "y",
+ "field": "y",
+ },
+ "value": "48",
+ },
+ {
+ "label": "x",
+ "role": "x",
+ "source": {
+ "channel": "x",
+ "field": "x",
+ },
+ "value": "40",
+ },
+ ],
+ "title": undefined,
+ },
+ ],
+ [
+ "point-6",
+ {
+ "fields": [
+ {
+ "color": "#06b6d4",
+ "label": "y",
+ "role": "value",
+ "source": {
+ "channel": "y",
+ "field": "y",
+ },
+ "value": "85",
+ },
+ {
+ "label": "x",
+ "role": "x",
+ "source": {
+ "channel": "x",
+ "field": "x",
+ },
+ "value": "50",
+ },
+ ],
+ "title": undefined,
+ },
+ ],
+ [
+ "point-7",
+ {
+ "fields": [
+ {
+ "color": "#06b6d4",
+ "label": "y",
+ "role": "value",
+ "source": {
+ "channel": "y",
+ "field": "y",
+ },
+ "value": "22",
+ },
+ {
+ "label": "x",
+ "role": "x",
+ "source": {
+ "channel": "x",
+ "field": "x",
+ },
+ "value": "60",
+ },
+ ],
+ "title": undefined,
+ },
+ ],
+ [
+ "point-8",
+ {
+ "fields": [
+ {
+ "color": "#06b6d4",
+ "label": "y",
+ "role": "value",
+ "source": {
+ "channel": "y",
+ "field": "y",
+ },
+ "value": "59",
+ },
+ {
+ "label": "x",
+ "role": "x",
+ "source": {
+ "channel": "x",
+ "field": "x",
+ },
+ "value": "70",
+ },
+ ],
+ "title": undefined,
+ },
+ ],
+ [
+ "point-9",
+ {
+ "fields": [
+ {
+ "color": "#06b6d4",
+ "label": "y",
+ "role": "value",
+ "source": {
+ "channel": "y",
+ "field": "y",
+ },
+ "value": "96",
+ },
+ {
+ "label": "x",
+ "role": "x",
+ "source": {
+ "channel": "x",
+ "field": "x",
+ },
+ "value": "80",
+ },
+ ],
+ "title": undefined,
+ },
+ ],
+ [
+ "point-10",
+ {
+ "fields": [
+ {
+ "color": "#06b6d4",
+ "label": "y",
+ "role": "value",
+ "source": {
+ "channel": "y",
+ "field": "y",
+ },
+ "value": "33",
+ },
+ {
+ "label": "x",
+ "role": "x",
+ "source": {
+ "channel": "x",
+ "field": "x",
+ },
+ "value": "90",
+ },
+ ],
+ "title": undefined,
+ },
+ ],
+ [
+ "point-11",
+ {
+ "fields": [
+ {
+ "color": "#06b6d4",
+ "label": "y",
+ "role": "value",
+ "source": {
+ "channel": "y",
+ "field": "y",
+ },
+ "value": "70",
+ },
+ {
+ "label": "x",
+ "role": "x",
+ "source": {
+ "channel": "x",
+ "field": "x",
+ },
+ "value": "100",
+ },
+ ],
+ "title": undefined,
+ },
+ ],
+ [
+ "point-12",
+ {
+ "fields": [
+ {
+ "color": "#06b6d4",
+ "label": "y",
+ "role": "value",
+ "source": {
+ "channel": "y",
+ "field": "y",
+ },
+ "value": "7",
+ },
+ {
+ "label": "x",
+ "role": "x",
+ "source": {
+ "channel": "x",
+ "field": "x",
+ },
+ "value": "110",
+ },
+ ],
+ "title": undefined,
+ },
+ ],
+ ],
+ "watermark": false,
+ "xInvert": {
+ "bottomData": 110,
+ "bottomPixel": 549,
+ "scaleType": "linear",
+ "topData": 0,
+ "topPixel": 47.016000000000005,
+ },
+ "yInvert": {
+ "bottomData": 100,
+ "bottomPixel": 116.5,
+ "scaleType": "linear",
+ "topData": 0,
+ "topPixel": 376,
+ },
+}
+`;
+
exports[`compileChart snapshot (Step 7 oracle) > clipped-domain bar chart (data outside scale.domain filtered) 1`] = `
{
"a11y": {
@@ -36,19 +1422,19 @@ exports[`compileChart snapshot (Step 7 oracle) > clipped-domain bar chart (data
"animation": undefined,
"annotations": [],
"area": {
- "height": 296.1,
- "width": 540.058,
- "x": 36.942,
- "y": 60.9,
+ "height": 287.1,
+ "width": 539.336,
+ "x": 37.664,
+ "y": 68.9,
},
"axes": {
"x": {
"domainLine": undefined,
"end": {
"x": 577,
- "y": 357,
+ "y": 356,
},
- "extent": 26,
+ "extent": 27,
"gridlines": [],
"label": undefined,
"labelFlush": undefined,
@@ -64,14 +1450,14 @@ exports[`compileChart snapshot (Step 7 oracle) > clipped-domain bar chart (data
"offset": undefined,
"orient": undefined,
"start": {
- "x": 36.942,
- "y": 357,
+ "x": 37.664,
+ "y": 356,
},
"tickAngle": undefined,
"tickLabelStyle": {
"fill": "#71717a",
"fontFamily": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
- "fontSize": 11,
+ "fontSize": 12,
"fontVariant": "tabular-nums",
"fontWeight": 400,
"lineHeight": 1.2,
@@ -81,17 +1467,17 @@ exports[`compileChart snapshot (Step 7 oracle) > clipped-domain bar chart (data
"ticks": [
{
"label": "0",
- "position": 36.942,
+ "position": 37.664,
"value": 0,
},
{
"label": "20",
- "position": 252.9652,
+ "position": 253.3984,
"value": 20,
},
{
"label": "40",
- "position": 468.9884,
+ "position": 469.13280000000003,
"value": 40,
},
],
@@ -100,21 +1486,21 @@ exports[`compileChart snapshot (Step 7 oracle) > clipped-domain bar chart (data
"y": {
"domainLine": undefined,
"end": {
- "x": 36.942,
- "y": 357,
+ "x": 37.664,
+ "y": 356,
},
"gridlines": [
{
"major": true,
- "position": 300.05769230769226,
+ "position": 300.78846153846155,
},
{
"major": true,
- "position": 208.95000000000002,
+ "position": 212.45000000000002,
},
{
"major": true,
- "position": 117.84230769230768,
+ "position": 124.11153846153847,
},
],
"label": undefined,
@@ -131,14 +1517,14 @@ exports[`compileChart snapshot (Step 7 oracle) > clipped-domain bar chart (data
"offset": undefined,
"orient": undefined,
"start": {
- "x": 36.942,
- "y": 60.9,
+ "x": 37.664,
+ "y": 68.9,
},
"tickAngle": undefined,
"tickLabelStyle": {
"fill": "#71717a",
"fontFamily": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
- "fontSize": 11,
+ "fontSize": 12,
"fontVariant": "tabular-nums",
"fontWeight": 400,
"lineHeight": 1.2,
@@ -148,17 +1534,17 @@ exports[`compileChart snapshot (Step 7 oracle) > clipped-domain bar chart (data
"ticks": [
{
"label": "A",
- "position": 300.05769230769226,
+ "position": 300.78846153846155,
"value": "A",
},
{
"label": "B",
- "position": 208.95000000000002,
+ "position": 212.45000000000002,
"value": "B",
},
{
"label": "C",
- "position": 117.84230769230768,
+ "position": 124.11153846153847,
"value": "C",
},
],
@@ -168,6 +1554,7 @@ exports[`compileChart snapshot (Step 7 oracle) > clipped-domain bar chart (data
"chrome": {
"bottomAnchorY": 383,
"bottomHeight": 0,
+ "grownHeight": 0,
"title": {
"maxWidth": 561,
"style": {
@@ -182,7 +1569,7 @@ exports[`compileChart snapshot (Step 7 oracle) > clipped-domain bar chart (data
"x": 17,
"y": 17,
},
- "topHeight": 37.9,
+ "topHeight": 45.9,
},
"crosshair": false,
"dimensions": {
@@ -202,7 +1589,7 @@ exports[`compileChart snapshot (Step 7 oracle) > clipped-domain bar chart (data
"labelStyle": {
"fill": "#09090b",
"fontFamily": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
- "fontSize": 11,
+ "fontSize": 12,
"fontWeight": 400,
"lineHeight": 1.3,
},
@@ -225,7 +1612,7 @@ exports[`compileChart snapshot (Step 7 oracle) > clipped-domain bar chart (data
"labelStyle": {
"fill": "#09090b",
"fontFamily": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
- "fontSize": 11,
+ "fontSize": 12,
"fontWeight": 400,
"lineHeight": 1.3,
},
@@ -253,7 +1640,7 @@ exports[`compileChart snapshot (Step 7 oracle) > clipped-domain bar chart (data
"value": 10,
},
"fill": "#51bccc",
- "height": 68.33076923076923,
+ "height": 66.25384615384615,
"key": "|A",
"label": {
"connector": undefined,
@@ -261,7 +1648,7 @@ exports[`compileChart snapshot (Step 7 oracle) > clipped-domain bar chart (data
"style": {
"dominantBaseline": "central",
"fill": "#111111",
- "fontFamily": "system-ui, -apple-system, sans-serif",
+ "fontFamily": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
"fontSize": 11,
"fontVariant": "tabular-nums",
"fontWeight": 600,
@@ -270,14 +1657,14 @@ exports[`compileChart snapshot (Step 7 oracle) > clipped-domain bar chart (data
},
"text": "10",
"visible": true,
- "x": 138.9536,
- "y": 300.05769230769226,
+ "x": 139.5312,
+ "y": 300.78846153846155,
},
"orient": "horizontal",
"type": "rect",
- "width": 108.01159999999999,
- "x": 36.942,
- "y": 265.89230769230767,
+ "width": 107.86720000000001,
+ "x": 37.664,
+ "y": 267.6615384615385,
},
{
"aria": {
@@ -295,7 +1682,7 @@ exports[`compileChart snapshot (Step 7 oracle) > clipped-domain bar chart (data
"value": 25,
},
"fill": "#51bccc",
- "height": 68.33076923076923,
+ "height": 66.25384615384615,
"key": "|B",
"label": {
"connector": undefined,
@@ -303,7 +1690,7 @@ exports[`compileChart snapshot (Step 7 oracle) > clipped-domain bar chart (data
"style": {
"dominantBaseline": "central",
"fill": "#111111",
- "fontFamily": "system-ui, -apple-system, sans-serif",
+ "fontFamily": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
"fontSize": 11,
"fontVariant": "tabular-nums",
"fontWeight": 600,
@@ -312,14 +1699,14 @@ exports[`compileChart snapshot (Step 7 oracle) > clipped-domain bar chart (data
},
"text": "25",
"visible": true,
- "x": 300.971,
- "y": 208.95000000000002,
+ "x": 301.332,
+ "y": 212.45000000000002,
},
"orient": "horizontal",
"type": "rect",
- "width": 270.029,
- "x": 36.942,
- "y": 174.7846153846154,
+ "width": 269.668,
+ "x": 37.664,
+ "y": 179.32307692307694,
},
{
"aria": {
@@ -337,7 +1724,7 @@ exports[`compileChart snapshot (Step 7 oracle) > clipped-domain bar chart (data
"value": 40,
},
"fill": "#51bccc",
- "height": 68.33076923076923,
+ "height": 66.25384615384615,
"key": "|C",
"label": {
"connector": undefined,
@@ -345,7 +1732,7 @@ exports[`compileChart snapshot (Step 7 oracle) > clipped-domain bar chart (data
"style": {
"dominantBaseline": "central",
"fill": "#111111",
- "fontFamily": "system-ui, -apple-system, sans-serif",
+ "fontFamily": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
"fontSize": 11,
"fontVariant": "tabular-nums",
"fontWeight": 600,
@@ -354,24 +1741,25 @@ exports[`compileChart snapshot (Step 7 oracle) > clipped-domain bar chart (data
},
"text": "40",
"visible": true,
- "x": 462.9884,
- "y": 117.84230769230768,
+ "x": 463.13280000000003,
+ "y": 124.11153846153847,
},
"orient": "horizontal",
"type": "rect",
- "width": 432.0464,
- "x": 36.942,
- "y": 83.67692307692306,
+ "width": 431.46880000000004,
+ "x": 37.664,
+ "y": 90.9846153846154,
},
],
"metrics": undefined,
"theme": {
+ "_authored": {},
"_tokenPairs": undefined,
"borderRadius": 8,
"chrome": {
"byline": {
"color": "#71717a",
- "fontSize": 11,
+ "fontSize": 12,
"fontWeight": 400,
"lineHeight": 1.4,
},
@@ -383,13 +1771,13 @@ exports[`compileChart snapshot (Step 7 oracle) > clipped-domain bar chart (data
},
"footer": {
"color": "#71717a",
- "fontSize": 11,
+ "fontSize": 12,
"fontWeight": 400,
"lineHeight": 1.4,
},
"source": {
"color": "#71717a",
- "fontSize": 11,
+ "fontSize": 12,
"fontWeight": 400,
"lineHeight": 1.4,
},
@@ -413,8 +1801,8 @@ exports[`compileChart snapshot (Step 7 oracle) > clipped-domain bar chart (data
"annotationMarkerFill": "#ffffff",
"annotationMuted": "#6b7280",
"annotationPlate": "#ffffff",
- "annotationRangeFill": "#000000",
- "annotationRule": "#666666",
+ "annotationRangeFill": "#f0c040",
+ "annotationRule": "#888888",
"annotationText": "#71717a",
"axis": "#71717a",
"background": "transparent",
@@ -454,8 +1842,11 @@ exports[`compileChart snapshot (Step 7 oracle) > clipped-domain bar chart (data
"#2166ac",
],
},
- "gridline": "rgba(0,0,0,0.08)",
+ "gridline": "rgba(0,0,0,0.05)",
+ "gridlineZero": "rgba(0,0,0,0.22)",
"hairline": "rgba(0,0,0,0.14)",
+ "mapInteriorBorder": "rgba(0,0,0,0.14)",
+ "mapOutlineBorder": "rgba(0,0,0,0.40)",
"negative": "#dc2626",
"neutral": {
"100": "#f0f0f0",
@@ -519,11 +1910,13 @@ exports[`compileChart snapshot (Step 7 oracle) > clipped-domain bar chart (data
"family": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
"mono": ""JetBrains Mono", "Fira Code", "Cascadia Code", monospace",
"sizes": {
- "axisTick": 11,
+ "axisTick": 12,
"body": 13,
+ "endpointLabel": 13,
+ "endpointValue": 12,
"metricLabel": 11,
"metricValue": 22,
- "small": 11,
+ "small": 12,
"subtitle": 14,
"title": 26,
},
@@ -539,9 +1932,12 @@ exports[`compileChart snapshot (Step 7 oracle) > clipped-domain bar chart (data
"seriesStrategy": "palette",
"spacing": {
"axisMargin": 6,
- "chartToFooter": 8,
+ "chartToFooter": 12,
"chromeGap": 6,
- "chromeToChart": 8,
+ "chromeToChart": 16,
+ "endpointColumnGap": 18,
+ "endpointEntryGap": 6,
+ "endpointValueGap": 4,
"padding": 20,
"xAxisHeight": 26,
"xAxisLabelPadding": 4,
@@ -554,6 +1950,11 @@ exports[`compileChart snapshot (Step 7 oracle) > clipped-domain bar chart (data
"fields": [
{
"label": "value",
+ "role": "value",
+ "source": {
+ "channel": "x",
+ "field": "value",
+ },
"value": "10",
},
],
@@ -566,6 +1967,11 @@ exports[`compileChart snapshot (Step 7 oracle) > clipped-domain bar chart (data
"fields": [
{
"label": "value",
+ "role": "value",
+ "source": {
+ "channel": "x",
+ "field": "value",
+ },
"value": "25",
},
],
@@ -578,6 +1984,11 @@ exports[`compileChart snapshot (Step 7 oracle) > clipped-domain bar chart (data
"fields": [
{
"label": "value",
+ "role": "value",
+ "source": {
+ "channel": "x",
+ "field": "value",
+ },
"value": "40",
},
],
@@ -591,23 +2002,23 @@ exports[`compileChart snapshot (Step 7 oracle) > clipped-domain bar chart (data
"bottomPixel": 577,
"scaleType": "linear",
"topData": 0,
- "topPixel": 36.942,
+ "topPixel": 37.664,
},
"yInvert": {
"bottomData": 0,
- "bottomPixel": 60.9,
+ "bottomPixel": 68.9,
"domain": [
"A",
"B",
"C",
],
"positions": [
- 300.05769230769226,
- 208.95000000000002,
- 117.84230769230768,
+ 300.78846153846155,
+ 212.45000000000002,
+ 124.11153846153847,
],
"topData": 0,
- "topPixel": 357,
+ "topPixel": 356,
},
}
`;
@@ -669,10 +2080,10 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
"animation": undefined,
"annotations": [],
"area": {
- "height": 263.4,
- "width": 683.04015,
+ "height": 248,
+ "width": 681.04015,
"x": 26,
- "y": 147.2,
+ "y": 156.2,
},
"axes": {
"x": undefined,
@@ -680,36 +2091,36 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
"domainLine": false,
"end": {
"x": 26,
- "y": 410.59999999999997,
+ "y": 404.2,
},
"gridlines": [
{
"major": true,
- "position": 394.13749999999993,
+ "position": 388.7,
},
{
"major": true,
- "position": 352.98125,
+ "position": 349.95,
},
{
"major": true,
- "position": 311.825,
+ "position": 311.2,
},
{
"major": true,
- "position": 270.66874999999993,
+ "position": 272.45,
},
{
"major": true,
- "position": 229.5125,
+ "position": 233.7,
},
{
"major": true,
- "position": 188.35625,
+ "position": 194.95,
},
{
"major": true,
- "position": 147.2,
+ "position": 156.2,
},
],
"label": undefined,
@@ -727,13 +2138,13 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
"orient": undefined,
"start": {
"x": 26,
- "y": 147.2,
+ "y": 156.2,
},
"tickAngle": undefined,
"tickLabelStyle": {
"fill": "#71717a",
"fontFamily": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
- "fontSize": 11,
+ "fontSize": 12,
"fontVariant": "tabular-nums",
"fontWeight": 400,
"lineHeight": 1.2,
@@ -743,37 +2154,37 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
"ticks": [
{
"label": "10",
- "position": 394.13749999999993,
+ "position": 388.7,
"value": 10,
},
{
"label": "15",
- "position": 352.98125,
+ "position": 349.95,
"value": 15,
},
{
"label": "20",
- "position": 311.825,
+ "position": 311.2,
"value": 20,
},
{
"label": "25",
- "position": 270.66874999999993,
+ "position": 272.45,
"value": 25,
},
{
"label": "30",
- "position": 229.5125,
+ "position": 233.7,
"value": 30,
},
{
"label": "35",
- "position": 188.35625,
+ "position": 194.95,
"value": 35,
},
{
"label": "40",
- "position": 147.2,
+ "position": 156.2,
"value": 40,
},
],
@@ -781,21 +2192,23 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
},
},
"chrome": {
- "bottomAnchorY": 436.59999999999997,
- "bottomHeight": 43.4,
+ "attributionLayout": "inline",
+ "bottomAnchorY": 431.2,
+ "bottomHeight": 48.8,
+ "grownHeight": 0,
"source": {
"maxWidth": 755,
"style": {
"fill": "#71717a",
"fontFamily": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
- "fontSize": 11,
+ "fontSize": 12,
"fontWeight": 400,
"lineHeight": 1.4,
"textAnchor": "start",
},
"text": "World Bank",
"x": 20,
- "y": 8,
+ "y": 12,
},
"subtitle": {
"maxWidth": 755,
@@ -825,7 +2238,7 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
"x": 20,
"y": 20,
},
- "topHeight": 64.2,
+ "topHeight": 72.2,
},
"crosshair": true,
"dimensions": {
@@ -835,94 +2248,139 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
"display": "full",
"endpointLabels": {
"bounds": {
- "height": 263.4,
+ "height": 248,
"width": 48.95985,
- "x": 727.04015,
- "y": 147.2,
+ "x": 725.04015,
+ "y": 156.2,
},
"entries": [
{
"color": "#048398",
- "dataY": 147.2,
+ "dataX": 707.04015,
+ "dataY": 156.2,
+ "date": undefined,
"labelLines": [
"US",
],
- "labelY": 147.2,
+ "labelY": 156.2,
"marker": {
- "dataX": 709.04015,
- "fill": "transparent",
+ "dataX": 707.04015,
+ "fill": "#ffffff",
"radius": 4,
"stroke": "#048398",
"strokeWidth": 2,
- "x": 713.04015,
- "y": 147.2,
+ "x": 707.04015,
+ "y": 156.2,
},
"seriesKey": "US",
"showLeader": false,
+ "source": {
+ "datum": {
+ "country": "US",
+ "date": "2021-01-01",
+ "value": 40,
+ },
+ "observed": true,
+ "x": "2021-01-01",
+ },
"value": "40",
},
{
"color": "#cc8020",
- "dataY": 188.35625,
+ "dataX": 707.04015,
+ "dataY": 194.95,
+ "date": undefined,
"labelLines": [
"UK",
],
- "labelY": 181.95,
+ "labelY": 197.45,
"marker": {
- "dataX": 709.04015,
- "fill": "transparent",
+ "dataX": 707.04015,
+ "fill": "#ffffff",
"radius": 4,
"stroke": "#cc8020",
"strokeWidth": 2,
- "x": 713.04015,
- "y": 188.35625,
+ "x": 707.04015,
+ "y": 194.95,
},
"seriesKey": "UK",
"showLeader": false,
+ "source": {
+ "datum": {
+ "country": "UK",
+ "date": "2021-01-01",
+ "value": 35,
+ },
+ "observed": true,
+ "x": "2021-01-01",
+ },
"value": "35",
},
{
"color": "#2c55a2",
- "dataY": 295.36249999999995,
+ "dataX": 707.04015,
+ "dataY": 295.7,
+ "date": undefined,
"labelLines": [
"FR",
],
- "labelY": 288.86249999999995,
+ "labelY": 289.2,
"marker": {
- "dataX": 709.04015,
- "fill": "transparent",
+ "dataX": 707.04015,
+ "fill": "#ffffff",
"radius": 4,
"stroke": "#2c55a2",
"strokeWidth": 2,
- "x": 713.04015,
- "y": 295.36249999999995,
+ "x": 707.04015,
+ "y": 295.7,
},
"seriesKey": "FR",
"showLeader": false,
+ "source": {
+ "datum": {
+ "country": "FR",
+ "date": "2021-01-01",
+ "value": 22,
+ },
+ "observed": true,
+ "x": "2021-01-01",
+ },
"value": "22",
},
{
"color": "#85315f",
- "dataY": 245.975,
+ "dataX": 707.04015,
+ "dataY": 249.2,
+ "date": undefined,
"labelLines": [
"DE",
],
- "labelY": 239.475,
+ "labelY": 242.7,
"marker": {
- "dataX": 709.04015,
- "fill": "transparent",
+ "dataX": 707.04015,
+ "fill": "#ffffff",
"radius": 4,
"stroke": "#85315f",
"strokeWidth": 2,
- "x": 713.04015,
- "y": 245.975,
+ "x": 707.04015,
+ "y": 249.2,
},
"seriesKey": "DE",
"showLeader": false,
+ "source": {
+ "datum": {
+ "country": "DE",
+ "date": "2021-01-01",
+ "value": 28,
+ },
+ "observed": true,
+ "x": "2021-01-01",
+ },
"value": "28",
},
],
"gap": 8,
+ "interior": undefined,
"labelStyle": {
"fill": "#09090b",
"fontFamily": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
@@ -947,9 +2405,9 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
"legend": {
"bounds": {
"height": 32,
- "width": 196.5,
+ "width": 202.00000000000003,
"x": 26,
- "y": 90.19999999999999,
+ "y": 98.19999999999999,
},
"entries": [
{
@@ -981,41 +2439,41 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
"entryPositions": [
{
"labelX": 44,
- "labelY": 96.19999999999999,
+ "labelY": 104.19999999999999,
"row": 0,
- "width": 49.278999999999996,
+ "width": 50.668,
"x": 26,
- "y": 90.19999999999999,
+ "y": 98.19999999999999,
},
{
- "labelX": 93.279,
- "labelY": 96.19999999999999,
+ "labelX": 94.668,
+ "labelY": 104.19999999999999,
"row": 0,
- "width": 49.278999999999996,
- "x": 75.279,
- "y": 90.19999999999999,
+ "width": 50.668,
+ "x": 76.668,
+ "y": 98.19999999999999,
},
{
- "labelX": 142.558,
- "labelY": 96.19999999999999,
+ "labelX": 145.336,
+ "labelY": 104.19999999999999,
"row": 0,
- "width": 48.663,
- "x": 124.55799999999999,
- "y": 90.19999999999999,
+ "width": 49.995999999999995,
+ "x": 127.336,
+ "y": 98.19999999999999,
},
{
- "labelX": 191.221,
- "labelY": 96.19999999999999,
+ "labelX": 195.332,
+ "labelY": 104.19999999999999,
"row": 0,
- "width": 49.278999999999996,
- "x": 173.221,
- "y": 90.19999999999999,
+ "width": 50.668,
+ "x": 177.332,
+ "y": 98.19999999999999,
},
],
"labelStyle": {
"fill": "#09090b",
"fontFamily": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
- "fontSize": 11,
+ "fontSize": 12,
"fontVariant": "tabular-nums",
"fontWeight": 400,
"lineHeight": 1.3,
@@ -1031,9 +2489,9 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
{
"bounds": {
"height": 32,
- "width": 196.5,
+ "width": 202.00000000000003,
"x": 26,
- "y": 90.19999999999999,
+ "y": 98.19999999999999,
},
"entries": [
{
@@ -1065,41 +2523,41 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
"entryPositions": [
{
"labelX": 44,
- "labelY": 96.19999999999999,
+ "labelY": 104.19999999999999,
"row": 0,
- "width": 49.278999999999996,
+ "width": 50.668,
"x": 26,
- "y": 90.19999999999999,
+ "y": 98.19999999999999,
},
{
- "labelX": 93.279,
- "labelY": 96.19999999999999,
+ "labelX": 94.668,
+ "labelY": 104.19999999999999,
"row": 0,
- "width": 49.278999999999996,
- "x": 75.279,
- "y": 90.19999999999999,
+ "width": 50.668,
+ "x": 76.668,
+ "y": 98.19999999999999,
},
{
- "labelX": 142.558,
- "labelY": 96.19999999999999,
+ "labelX": 145.336,
+ "labelY": 104.19999999999999,
"row": 0,
- "width": 48.663,
- "x": 124.55799999999999,
- "y": 90.19999999999999,
+ "width": 49.995999999999995,
+ "x": 127.336,
+ "y": 98.19999999999999,
},
{
- "labelX": 191.221,
- "labelY": 96.19999999999999,
+ "labelX": 195.332,
+ "labelY": 104.19999999999999,
"row": 0,
- "width": 49.278999999999996,
- "x": 173.221,
- "y": 90.19999999999999,
+ "width": 50.668,
+ "x": 177.332,
+ "y": 98.19999999999999,
},
],
"labelStyle": {
"fill": "#09090b",
"fontFamily": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
- "fontSize": 11,
+ "fontSize": 12,
"fontVariant": "tabular-nums",
"fontWeight": 400,
"lineHeight": 1.3,
@@ -1141,18 +2599,28 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
{
"color": "#048398",
"label": "country",
+ "role": "series",
+ "source": {
+ "channel": "color",
+ "field": "country",
+ },
"value": "US",
},
{
"color": undefined,
"label": "value",
+ "role": "value",
+ "source": {
+ "channel": "y",
+ "field": "value",
+ },
"value": "10",
},
],
"title": "2020",
},
- "x": 47,
- "y": 394.13749999999993,
+ "x": 48,
+ "y": 388.7,
},
{
"datum": {
@@ -1165,36 +2633,46 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
{
"color": "#048398",
"label": "country",
+ "role": "series",
+ "source": {
+ "channel": "color",
+ "field": "country",
+ },
"value": "US",
},
{
"color": undefined,
"label": "value",
+ "role": "value",
+ "source": {
+ "channel": "y",
+ "field": "value",
+ },
"value": "40",
},
],
"title": "2021",
},
- "x": 709.04015,
- "y": 147.2,
+ "x": 707.04015,
+ "y": 156.2,
},
],
"interpolate": "monotone",
"key": "US",
"opacity": undefined,
- "path": "M47,394.137L709.04,147.2",
+ "path": "M48,388.7L707.04,156.2",
"pointKeys": [
"2020-01-01",
"2021-01-01",
],
"points": [
{
- "x": 47,
- "y": 394.13749999999993,
+ "x": 48,
+ "y": 388.7,
},
{
- "x": 709.04015,
- "y": 147.2,
+ "x": 707.04015,
+ "y": 156.2,
},
],
"seriesKey": "US",
@@ -1231,18 +2709,28 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
{
"color": "#cc8020",
"label": "country",
+ "role": "series",
+ "source": {
+ "channel": "color",
+ "field": "country",
+ },
"value": "UK",
},
{
"color": undefined,
"label": "value",
+ "role": "value",
+ "source": {
+ "channel": "y",
+ "field": "value",
+ },
"value": "15",
},
],
"title": "2020",
},
- "x": 47,
- "y": 352.98125,
+ "x": 48,
+ "y": 349.95,
},
{
"datum": {
@@ -1255,36 +2743,46 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
{
"color": "#cc8020",
"label": "country",
+ "role": "series",
+ "source": {
+ "channel": "color",
+ "field": "country",
+ },
"value": "UK",
},
{
"color": undefined,
"label": "value",
+ "role": "value",
+ "source": {
+ "channel": "y",
+ "field": "value",
+ },
"value": "35",
},
],
"title": "2021",
},
- "x": 709.04015,
- "y": 188.35625,
+ "x": 707.04015,
+ "y": 194.95,
},
],
"interpolate": "monotone",
"key": "UK",
"opacity": undefined,
- "path": "M47,352.981L709.04,188.356",
+ "path": "M48,349.95L707.04,194.95",
"pointKeys": [
"2020-01-01",
"2021-01-01",
],
"points": [
{
- "x": 47,
- "y": 352.98125,
+ "x": 48,
+ "y": 349.95,
},
{
- "x": 709.04015,
- "y": 188.35625,
+ "x": 707.04015,
+ "y": 194.95,
},
],
"seriesKey": "UK",
@@ -1321,18 +2819,28 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
{
"color": "#2c55a2",
"label": "country",
+ "role": "series",
+ "source": {
+ "channel": "color",
+ "field": "country",
+ },
"value": "FR",
},
{
"color": undefined,
"label": "value",
+ "role": "value",
+ "source": {
+ "channel": "y",
+ "field": "value",
+ },
"value": "8",
},
],
"title": "2020",
},
- "x": 47,
- "y": 410.59999999999997,
+ "x": 48,
+ "y": 404.2,
},
{
"datum": {
@@ -1345,36 +2853,46 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
{
"color": "#2c55a2",
"label": "country",
+ "role": "series",
+ "source": {
+ "channel": "color",
+ "field": "country",
+ },
"value": "FR",
},
{
"color": undefined,
"label": "value",
+ "role": "value",
+ "source": {
+ "channel": "y",
+ "field": "value",
+ },
"value": "22",
},
],
"title": "2021",
},
- "x": 709.04015,
- "y": 295.36249999999995,
+ "x": 707.04015,
+ "y": 295.7,
},
],
"interpolate": "monotone",
"key": "FR",
"opacity": undefined,
- "path": "M47,410.6L709.04,295.362",
+ "path": "M48,404.2L707.04,295.7",
"pointKeys": [
"2020-01-01",
"2021-01-01",
],
"points": [
{
- "x": 47,
- "y": 410.59999999999997,
+ "x": 48,
+ "y": 404.2,
},
{
- "x": 709.04015,
- "y": 295.36249999999995,
+ "x": 707.04015,
+ "y": 295.7,
},
],
"seriesKey": "FR",
@@ -1411,18 +2929,28 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
{
"color": "#85315f",
"label": "country",
+ "role": "series",
+ "source": {
+ "channel": "color",
+ "field": "country",
+ },
"value": "DE",
},
{
"color": undefined,
"label": "value",
+ "role": "value",
+ "source": {
+ "channel": "y",
+ "field": "value",
+ },
"value": "12",
},
],
"title": "2020",
},
- "x": 47,
- "y": 377.67499999999995,
+ "x": 48,
+ "y": 373.2,
},
{
"datum": {
@@ -1435,36 +2963,46 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
{
"color": "#85315f",
"label": "country",
+ "role": "series",
+ "source": {
+ "channel": "color",
+ "field": "country",
+ },
"value": "DE",
},
{
"color": undefined,
"label": "value",
+ "role": "value",
+ "source": {
+ "channel": "y",
+ "field": "value",
+ },
"value": "28",
},
],
"title": "2021",
},
- "x": 709.04015,
- "y": 245.975,
+ "x": 707.04015,
+ "y": 249.2,
},
],
"interpolate": "monotone",
"key": "DE",
"opacity": undefined,
- "path": "M47,377.675L709.04,245.975",
+ "path": "M48,373.2L707.04,249.2",
"pointKeys": [
"2020-01-01",
"2021-01-01",
],
"points": [
{
- "x": 47,
- "y": 377.67499999999995,
+ "x": 48,
+ "y": 373.2,
},
{
- "x": 709.04015,
- "y": 245.975,
+ "x": 707.04015,
+ "y": 249.2,
},
],
"seriesKey": "DE",
@@ -1476,12 +3014,13 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
],
"metrics": undefined,
"theme": {
+ "_authored": {},
"_tokenPairs": undefined,
"borderRadius": 8,
"chrome": {
"byline": {
"color": "#71717a",
- "fontSize": 11,
+ "fontSize": 12,
"fontWeight": 400,
"lineHeight": 1.4,
},
@@ -1493,13 +3032,13 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
},
"footer": {
"color": "#71717a",
- "fontSize": 11,
+ "fontSize": 12,
"fontWeight": 400,
"lineHeight": 1.4,
},
"source": {
"color": "#71717a",
- "fontSize": 11,
+ "fontSize": 12,
"fontWeight": 400,
"lineHeight": 1.4,
},
@@ -1523,8 +3062,8 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
"annotationMarkerFill": "#ffffff",
"annotationMuted": "#6b7280",
"annotationPlate": "#ffffff",
- "annotationRangeFill": "#000000",
- "annotationRule": "#666666",
+ "annotationRangeFill": "#f0c040",
+ "annotationRule": "#888888",
"annotationText": "#71717a",
"axis": "#71717a",
"background": "transparent",
@@ -1564,8 +3103,11 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
"#2166ac",
],
},
- "gridline": "rgba(0,0,0,0.08)",
+ "gridline": "rgba(0,0,0,0.05)",
+ "gridlineZero": "rgba(0,0,0,0.22)",
"hairline": "rgba(0,0,0,0.14)",
+ "mapInteriorBorder": "rgba(0,0,0,0.14)",
+ "mapOutlineBorder": "rgba(0,0,0,0.40)",
"negative": "#dc2626",
"neutral": {
"100": "#f0f0f0",
@@ -1629,11 +3171,13 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
"family": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
"mono": ""JetBrains Mono", "Fira Code", "Cascadia Code", monospace",
"sizes": {
- "axisTick": 11,
+ "axisTick": 12,
"body": 13,
+ "endpointLabel": 13,
+ "endpointValue": 12,
"metricLabel": 11,
"metricValue": 22,
- "small": 11,
+ "small": 12,
"subtitle": 14,
"title": 26,
},
@@ -1649,9 +3193,12 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
"seriesStrategy": "palette",
"spacing": {
"axisMargin": 6,
- "chartToFooter": 8,
+ "chartToFooter": 12,
"chromeGap": 6,
- "chromeToChart": 8,
+ "chromeToChart": 16,
+ "endpointColumnGap": 18,
+ "endpointEntryGap": 6,
+ "endpointValueGap": 4,
"padding": 20,
"xAxisHeight": 26,
"xAxisLabelPadding": 4,
@@ -1661,17 +3208,17 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
"watermark": false,
"xInvert": {
"bottomData": 1609459200000,
- "bottomPixel": 709.04015,
- "scaleType": "time",
+ "bottomPixel": 707.04015,
+ "scaleType": "utc",
"topData": 1577836800000,
- "topPixel": 47,
+ "topPixel": 48,
},
"yInvert": {
"bottomData": 40,
- "bottomPixel": 147.2,
+ "bottomPixel": 156.2,
"scaleType": "linear",
"topData": 8,
- "topPixel": 410.59999999999997,
+ "topPixel": 404.2,
},
}
`;
@@ -1708,19 +3255,19 @@ exports[`compileChart snapshot (Step 7 oracle) > watermarked column chart with g
"animation": undefined,
"annotations": [],
"area": {
- "height": 234.3,
- "width": 537.768,
- "x": 39.232,
- "y": 60.9,
+ "height": 218.50000000000003,
+ "width": 536.656,
+ "x": 40.344,
+ "y": 68.9,
},
"axes": {
"x": {
"domainLine": undefined,
"end": {
"x": 577,
- "y": 295.2,
+ "y": 287.40000000000003,
},
- "extent": 26,
+ "extent": 27,
"gridlines": [],
"label": undefined,
"labelFlush": undefined,
@@ -1736,14 +3283,14 @@ exports[`compileChart snapshot (Step 7 oracle) > watermarked column chart with g
"offset": undefined,
"orient": undefined,
"start": {
- "x": 39.232,
- "y": 295.2,
+ "x": 40.344,
+ "y": 287.40000000000003,
},
"tickAngle": undefined,
"tickLabelStyle": {
"fill": "#71717a",
"fontFamily": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
- "fontSize": 11,
+ "fontSize": 12,
"fontVariant": "tabular-nums",
"fontWeight": 400,
"lineHeight": 1.2,
@@ -1753,22 +3300,22 @@ exports[`compileChart snapshot (Step 7 oracle) > watermarked column chart with g
"ticks": [
{
"label": "A",
- "position": 118.31552941176471,
+ "position": 119.26399999999998,
"value": "A",
},
{
"label": "B",
- "position": 244.84917647058825,
+ "position": 245.53599999999997,
"value": "B",
},
{
"label": "C",
- "position": 371.3828235294118,
+ "position": 371.80799999999994,
"value": "C",
},
{
"label": "D",
- "position": 497.91647058823537,
+ "position": 498.0799999999999,
"value": "D",
},
],
@@ -1777,41 +3324,42 @@ exports[`compileChart snapshot (Step 7 oracle) > watermarked column chart with g
"y": {
"domainLine": undefined,
"end": {
- "x": 39.232,
- "y": 295.2,
+ "x": 40.344,
+ "y": 287.40000000000003,
},
"gridlines": [
{
+ "color": "rgba(0,0,0,0.22)",
"major": true,
- "position": 295.2,
+ "position": 287.40000000000003,
},
{
"major": true,
- "position": 261.72857142857146,
+ "position": 256.1857142857143,
},
{
"major": true,
- "position": 228.25714285714287,
+ "position": 224.9714285714286,
},
{
"major": true,
- "position": 194.78571428571428,
+ "position": 193.75714285714287,
},
{
"major": true,
- "position": 161.31428571428572,
+ "position": 162.54285714285717,
},
{
"major": true,
- "position": 127.84285714285714,
+ "position": 131.32857142857142,
},
{
"major": true,
- "position": 94.37142857142858,
+ "position": 100.11428571428573,
},
{
"major": true,
- "position": 60.9,
+ "position": 68.9,
},
],
"label": undefined,
@@ -1828,14 +3376,14 @@ exports[`compileChart snapshot (Step 7 oracle) > watermarked column chart with g
"offset": undefined,
"orient": undefined,
"start": {
- "x": 39.232,
- "y": 60.9,
+ "x": 40.344,
+ "y": 68.9,
},
"tickAngle": undefined,
"tickLabelStyle": {
"fill": "#71717a",
"fontFamily": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
- "fontSize": 11,
+ "fontSize": 12,
"fontVariant": "tabular-nums",
"fontWeight": 400,
"lineHeight": 1.2,
@@ -1845,42 +3393,42 @@ exports[`compileChart snapshot (Step 7 oracle) > watermarked column chart with g
"ticks": [
{
"label": "0",
- "position": 295.2,
+ "position": 287.40000000000003,
"value": 0,
},
{
"label": "10",
- "position": 261.72857142857146,
+ "position": 256.1857142857143,
"value": 10,
},
{
"label": "20",
- "position": 228.25714285714287,
+ "position": 224.9714285714286,
"value": 20,
},
{
"label": "30",
- "position": 194.78571428571428,
+ "position": 193.75714285714287,
"value": 30,
},
{
"label": "40",
- "position": 161.31428571428572,
+ "position": 162.54285714285717,
"value": 40,
},
{
"label": "50",
- "position": 127.84285714285714,
+ "position": 131.32857142857142,
"value": 50,
},
{
"label": "60",
- "position": 94.37142857142858,
+ "position": 100.11428571428573,
"value": 60,
},
{
"label": "70",
- "position": 60.9,
+ "position": 68.9,
"value": 70,
},
],
@@ -1888,35 +3436,37 @@ exports[`compileChart snapshot (Step 7 oracle) > watermarked column chart with g
},
},
"chrome": {
- "bottomAnchorY": 321.2,
- "bottomHeight": 61.8,
+ "attributionLayout": "inline",
+ "bottomAnchorY": 314.40000000000003,
+ "bottomHeight": 68.6,
"byline": {
- "maxWidth": 431,
+ "maxWidth": 500.576,
"style": {
"fill": "#71717a",
"fontFamily": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
- "fontSize": 11,
+ "fontSize": 12,
"fontWeight": 400,
"lineHeight": 1.4,
"textAnchor": "start",
},
"text": "By the engine",
"x": 17,
- "y": 29.4,
+ "y": 34.8,
},
+ "grownHeight": 0,
"source": {
- "maxWidth": 431,
+ "maxWidth": 500.576,
"style": {
"fill": "#71717a",
"fontFamily": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
- "fontSize": 11,
+ "fontSize": 12,
"fontWeight": 400,
"lineHeight": 1.4,
"textAnchor": "start",
},
"text": "Test data",
"x": 17,
- "y": 8,
+ "y": 12,
},
"title": {
"maxWidth": 561,
@@ -1932,7 +3482,21 @@ exports[`compileChart snapshot (Step 7 oracle) > watermarked column chart with g
"x": 17,
"y": 17,
},
- "topHeight": 37.9,
+ "topHeight": 45.9,
+ "watermark": {
+ "maxWidth": 53.42400000000001,
+ "style": {
+ "fill": "#71717a",
+ "fontFamily": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
+ "fontSize": 12,
+ "fontWeight": 600,
+ "lineHeight": 1.3,
+ "textAnchor": "end",
+ },
+ "text": "OpenData",
+ "x": 583,
+ "y": 12,
+ },
},
"crosshair": false,
"dimensions": {
@@ -1952,7 +3516,7 @@ exports[`compileChart snapshot (Step 7 oracle) > watermarked column chart with g
"labelStyle": {
"fill": "#09090b",
"fontFamily": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
- "fontSize": 11,
+ "fontSize": 12,
"fontWeight": 400,
"lineHeight": 1.3,
},
@@ -1975,7 +3539,7 @@ exports[`compileChart snapshot (Step 7 oracle) > watermarked column chart with g
"labelStyle": {
"fill": "#09090b",
"fontFamily": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
- "fontSize": 11,
+ "fontSize": 12,
"fontWeight": 400,
"lineHeight": 1.3,
},
@@ -2015,13 +3579,13 @@ exports[`compileChart snapshot (Step 7 oracle) > watermarked column chart with g
],
"type": "linear",
},
- "height": 100.41428571428571,
+ "height": 93.64285714285717,
"key": "|A",
"label": {
"connector": undefined,
"style": {
"fill": "#0000ff",
- "fontFamily": "system-ui, -apple-system, sans-serif",
+ "fontFamily": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
"fontSize": 10,
"fontVariant": "tabular-nums",
"fontWeight": 600,
@@ -2030,14 +3594,14 @@ exports[`compileChart snapshot (Step 7 oracle) > watermarked column chart with g
},
"text": "30",
"visible": true,
- "x": 118.31552941176471,
- "y": 182.78571428571428,
+ "x": 119.26399999999998,
+ "y": 181.75714285714287,
},
"orient": "vertical",
"type": "rect",
- "width": 94.90023529411765,
- "x": 70.8654117647059,
- "y": 194.78571428571428,
+ "width": 94.704,
+ "x": 71.91199999999998,
+ "y": 193.75714285714287,
},
{
"aria": {
@@ -2067,13 +3631,13 @@ exports[`compileChart snapshot (Step 7 oracle) > watermarked column chart with g
],
"type": "linear",
},
- "height": 184.09285714285713,
+ "height": 171.67857142857144,
"key": "|B",
"label": {
"connector": undefined,
"style": {
"fill": "#0000ff",
- "fontFamily": "system-ui, -apple-system, sans-serif",
+ "fontFamily": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
"fontSize": 10,
"fontVariant": "tabular-nums",
"fontWeight": 600,
@@ -2082,14 +3646,14 @@ exports[`compileChart snapshot (Step 7 oracle) > watermarked column chart with g
},
"text": "55",
"visible": true,
- "x": 244.84917647058825,
- "y": 99.10714285714286,
+ "x": 245.53599999999997,
+ "y": 103.72142857142859,
},
"orient": "vertical",
"type": "rect",
- "width": 94.90023529411765,
- "x": 197.39905882352943,
- "y": 111.10714285714286,
+ "width": 94.704,
+ "x": 198.18399999999997,
+ "y": 115.72142857142859,
},
{
"aria": {
@@ -2119,13 +3683,13 @@ exports[`compileChart snapshot (Step 7 oracle) > watermarked column chart with g
],
"type": "linear",
},
- "height": 234.29999999999998,
+ "height": 218.50000000000003,
"key": "|C",
"label": {
"connector": undefined,
"style": {
"fill": "#0000ff",
- "fontFamily": "system-ui, -apple-system, sans-serif",
+ "fontFamily": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
"fontSize": 10,
"fontVariant": "tabular-nums",
"fontWeight": 600,
@@ -2134,14 +3698,14 @@ exports[`compileChart snapshot (Step 7 oracle) > watermarked column chart with g
},
"text": "70",
"visible": true,
- "x": 371.3828235294118,
- "y": 48.9,
+ "x": 371.80799999999994,
+ "y": 56.900000000000006,
},
"orient": "vertical",
"type": "rect",
- "width": 94.90023529411765,
- "x": 323.93270588235293,
- "y": 60.9,
+ "width": 94.704,
+ "x": 324.45599999999996,
+ "y": 68.9,
},
{
"aria": {
@@ -2171,13 +3735,13 @@ exports[`compileChart snapshot (Step 7 oracle) > watermarked column chart with g
],
"type": "linear",
},
- "height": 150.62142857142857,
+ "height": 140.46428571428575,
"key": "|D",
"label": {
"connector": undefined,
"style": {
"fill": "#0000ff",
- "fontFamily": "system-ui, -apple-system, sans-serif",
+ "fontFamily": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
"fontSize": 10,
"fontVariant": "tabular-nums",
"fontWeight": 600,
@@ -2186,24 +3750,25 @@ exports[`compileChart snapshot (Step 7 oracle) > watermarked column chart with g
},
"text": "45",
"visible": true,
- "x": 497.91647058823537,
- "y": 132.57857142857142,
+ "x": 498.0799999999999,
+ "y": 134.93571428571428,
},
"orient": "vertical",
"type": "rect",
- "width": 94.90023529411765,
- "x": 450.4663529411765,
- "y": 144.57857142857142,
+ "width": 94.704,
+ "x": 450.72799999999995,
+ "y": 146.93571428571428,
},
],
"metrics": undefined,
"theme": {
+ "_authored": {},
"_tokenPairs": undefined,
"borderRadius": 8,
"chrome": {
"byline": {
"color": "#71717a",
- "fontSize": 11,
+ "fontSize": 12,
"fontWeight": 400,
"lineHeight": 1.4,
},
@@ -2215,13 +3780,13 @@ exports[`compileChart snapshot (Step 7 oracle) > watermarked column chart with g
},
"footer": {
"color": "#71717a",
- "fontSize": 11,
+ "fontSize": 12,
"fontWeight": 400,
"lineHeight": 1.4,
},
"source": {
"color": "#71717a",
- "fontSize": 11,
+ "fontSize": 12,
"fontWeight": 400,
"lineHeight": 1.4,
},
@@ -2245,8 +3810,8 @@ exports[`compileChart snapshot (Step 7 oracle) > watermarked column chart with g
"annotationMarkerFill": "#ffffff",
"annotationMuted": "#6b7280",
"annotationPlate": "#ffffff",
- "annotationRangeFill": "#000000",
- "annotationRule": "#666666",
+ "annotationRangeFill": "#f0c040",
+ "annotationRule": "#888888",
"annotationText": "#71717a",
"axis": "#71717a",
"background": "transparent",
@@ -2286,8 +3851,11 @@ exports[`compileChart snapshot (Step 7 oracle) > watermarked column chart with g
"#2166ac",
],
},
- "gridline": "rgba(0,0,0,0.08)",
+ "gridline": "rgba(0,0,0,0.05)",
+ "gridlineZero": "rgba(0,0,0,0.22)",
"hairline": "rgba(0,0,0,0.14)",
+ "mapInteriorBorder": "rgba(0,0,0,0.14)",
+ "mapOutlineBorder": "rgba(0,0,0,0.40)",
"negative": "#dc2626",
"neutral": {
"100": "#f0f0f0",
@@ -2351,11 +3919,13 @@ exports[`compileChart snapshot (Step 7 oracle) > watermarked column chart with g
"family": ""Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif",
"mono": ""JetBrains Mono", "Fira Code", "Cascadia Code", monospace",
"sizes": {
- "axisTick": 11,
+ "axisTick": 12,
"body": 13,
+ "endpointLabel": 13,
+ "endpointValue": 12,
"metricLabel": 11,
"metricValue": 22,
- "small": 11,
+ "small": 12,
"subtitle": 14,
"title": 26,
},
@@ -2371,9 +3941,12 @@ exports[`compileChart snapshot (Step 7 oracle) > watermarked column chart with g
"seriesStrategy": "palette",
"spacing": {
"axisMargin": 6,
- "chartToFooter": 8,
+ "chartToFooter": 12,
"chromeGap": 6,
- "chromeToChart": 8,
+ "chromeToChart": 16,
+ "endpointColumnGap": 18,
+ "endpointEntryGap": 6,
+ "endpointValueGap": 4,
"padding": 20,
"xAxisHeight": 26,
"xAxisLabelPadding": 4,
@@ -2387,6 +3960,11 @@ exports[`compileChart snapshot (Step 7 oracle) > watermarked column chart with g
{
"color": "#0000ff",
"label": "score",
+ "role": "value",
+ "source": {
+ "channel": "y",
+ "field": "score",
+ },
"value": "30",
},
],
@@ -2400,6 +3978,11 @@ exports[`compileChart snapshot (Step 7 oracle) > watermarked column chart with g
{
"color": "#0000ff",
"label": "score",
+ "role": "value",
+ "source": {
+ "channel": "y",
+ "field": "score",
+ },
"value": "55",
},
],
@@ -2413,6 +3996,11 @@ exports[`compileChart snapshot (Step 7 oracle) > watermarked column chart with g
{
"color": "#0000ff",
"label": "score",
+ "role": "value",
+ "source": {
+ "channel": "y",
+ "field": "score",
+ },
"value": "70",
},
],
@@ -2426,6 +4014,11 @@ exports[`compileChart snapshot (Step 7 oracle) > watermarked column chart with g
{
"color": "#0000ff",
"label": "score",
+ "role": "value",
+ "source": {
+ "channel": "y",
+ "field": "score",
+ },
"value": "45",
},
],
@@ -2444,20 +4037,20 @@ exports[`compileChart snapshot (Step 7 oracle) > watermarked column chart with g
"D",
],
"positions": [
- 118.31552941176471,
- 244.84917647058825,
- 371.3828235294118,
- 497.91647058823537,
+ 119.26399999999998,
+ 245.53599999999997,
+ 371.80799999999994,
+ 498.0799999999999,
],
"topData": 0,
- "topPixel": 39.232,
+ "topPixel": 40.344,
},
"yInvert": {
"bottomData": 70,
- "bottomPixel": 60.9,
+ "bottomPixel": 68.9,
"scaleType": "linear",
"topData": 0,
- "topPixel": 295.2,
+ "topPixel": 287.40000000000003,
},
}
`;
diff --git a/packages/engine/src/__tests__/annotation-behavior.test.ts b/packages/engine/src/__tests__/annotation-behavior.test.ts
index 27768c4f..58948856 100644
--- a/packages/engine/src/__tests__/annotation-behavior.test.ts
+++ b/packages/engine/src/__tests__/annotation-behavior.test.ts
@@ -37,7 +37,6 @@ import {
DEFAULT_ANNOTATION_FONT_WEIGHT,
DEFAULT_DOT_RADIUS,
DEFAULT_DOT_STROKE_WIDTH,
- LEDE_FONT_WEIGHT,
MIN_CONNECTOR_LENGTH,
SUBTITLE_FONT_WEIGHT,
subtitleFontSize,
@@ -467,19 +466,19 @@ describe('the words are styled the way the design says', () => {
expect(a.label!.style.fontWeight).toBe(DEFAULT_ANNOTATION_FONT_WEIGHT);
});
- test('adding a subtitle promotes the first line to a bold lede', () => {
+ test('adding a subtitle preserves the regular primary weight', () => {
const a = first([
{ type: 'text', x: 'Mar', y: 30, text: '+1.17°C', subtitle: 'the warmest year' },
]);
- // This is the pairing the whole lede rule exists for. It once shipped dead.
- expect(a.label!.style.fontWeight).toBe(LEDE_FONT_WEIGHT);
+ // Both lines remain regular unless the author explicitly sets a weight.
+ expect(a.label!.style.fontWeight).toBe(DEFAULT_ANNOTATION_FONT_WEIGHT);
expect(a.subtitle!.text).toBe('the warmest year');
expect(a.subtitle!.style.fontWeight).toBe(SUBTITLE_FONT_WEIGHT);
expect(a.subtitle!.style.fontSize).toBe(subtitleFontSize(DEFAULT_ANNOTATION_FONT_SIZE));
});
- test('an explicit weight beats the lede rule', () => {
+ test('an explicit weight is preserved alongside a subtitle', () => {
const a = first([
{ type: 'text', x: 'Mar', y: 30, text: 'Peak', subtitle: 'context', fontWeight: 500 },
]);
@@ -498,7 +497,7 @@ describe('the words are styled the way the design says', () => {
{ type: 'text', x: 'Mar', y: 30, text: 'Peak', subtitle: 'context', connector: 'drop-line' },
]);
expect(a.subtitle?.text).toBe('context');
- expect(a.label!.style.fontWeight).toBe(LEDE_FONT_WEIGHT);
+ expect(a.label!.style.fontWeight).toBe(DEFAULT_ANNOTATION_FONT_WEIGHT);
});
test('text is never center-aligned; it faces the data', () => {
diff --git a/packages/engine/src/__tests__/axes.test.ts b/packages/engine/src/__tests__/axes.test.ts
index 9b187df6..f0d11d78 100644
--- a/packages/engine/src/__tests__/axes.test.ts
+++ b/packages/engine/src/__tests__/axes.test.ts
@@ -54,8 +54,11 @@ describe('computeAxes', () => {
const scales = computeScales(lineSpec, chartArea, lineSpec.data);
const axes = computeAxes(scales, chartArea, fullStrategy, theme);
- expect(axes.x).toBeDefined();
- expect(axes.y).toBeDefined();
+ // x runs along the bottom edge of the plot, y up the left edge.
+ expect(axes.x!.start.y).toBe(chartArea.y + chartArea.height);
+ expect(axes.x!.ticks.length).toBeGreaterThan(1);
+ expect(axes.y!.start.x).toBe(chartArea.x);
+ expect(axes.y!.ticks.length).toBeGreaterThan(1);
});
it('generates ticks for both axes', () => {
@@ -681,7 +684,7 @@ describe('text-aware tick density', () => {
it('applies the last-anchored stride when even vertical labels cannot fit', () => {
// 40 short categories on a 360px chart: ~9px steps fit nothing at any
// angle (a vertical label ribbon needs ~17px), so the safety net keeps
- // every 2nd label counting back from the LAST category.
+ // every 3rd label counting back from the LAST category.
const categories = Array.from({ length: 40 }, (_, i) => `C${i + 1}`);
const barSpec: NormalizedChartSpec = {
...lineSpec,
@@ -700,7 +703,7 @@ describe('text-aware tick density', () => {
expect(axes.x!.tickAngle).toBe(-90);
const kept = axes.x!.ticks.map((t) => t.label);
- expect(kept.length).toBe(20);
+ expect(kept).toEqual(Array.from({ length: 14 }, (_, i) => `C${1 + i * 3}`));
// The stride anchors at the final category — the most recent one is
// always labeled.
expect(kept[kept.length - 1]).toBe('C40');
@@ -1634,3 +1637,37 @@ describe('compact temporal ticks: fitting rung', () => {
expect(labels).toEqual(['2025-01', '2025-10']);
});
});
+
+it('separates two categorical endpoint labels in a compressed plot without moving ticks', () => {
+ const spec: NormalizedChartSpec = {
+ ...lineSpec,
+ data: [
+ { year: '2019', value: 1 },
+ { year: '2024', value: 2 },
+ ],
+ encoding: {
+ x: { field: 'year', type: 'ordinal' },
+ y: { field: 'value', type: 'quantitative' },
+ },
+ };
+ const area = { x: 150, y: 20, width: 18, height: 180 };
+ const scales = computeScales(spec, area, spec.data);
+ const axes = computeAxes(scales, area, fullStrategy, theme, undefined, {
+ data: spec.data,
+ encoding: spec.encoding as Encoding,
+ markType: 'line',
+ totalWidth: 320,
+ });
+ const [a, b] = axes.x!.ticks;
+ expect(a.position).toBe(150);
+ expect(b.position).toBe(168);
+ const half = (tick: AxisTick) =>
+ estimateTextWidth(
+ tick.label,
+ axes.x!.tickLabelStyle.fontSize,
+ axes.x!.tickLabelStyle.fontWeight,
+ ) / 2;
+ expect((a.labelPosition ?? a.position) + half(a) + 5).toBeLessThanOrEqual(
+ (b.labelPosition ?? b.position) - half(b),
+ );
+});
diff --git a/packages/engine/src/__tests__/axis-title-header.test.ts b/packages/engine/src/__tests__/axis-title-header.test.ts
new file mode 100644
index 00000000..303e065f
--- /dev/null
+++ b/packages/engine/src/__tests__/axis-title-header.test.ts
@@ -0,0 +1,116 @@
+import type { LayerSpec } from '@opendata-ai/openchart-core';
+import { describe, expect, it } from 'vitest';
+import { compileChart, compileLayer } from '../compile';
+
+const base = {
+ mark: 'line',
+ data: [
+ { date: '2024-01-01', value: 10 },
+ { date: '2024-02-01', value: 30 },
+ ],
+ encoding: {
+ x: { field: 'date', type: 'temporal' },
+ y: {
+ field: 'value',
+ type: 'quantitative',
+ axis: { title: 'Revenue per customer ($)', tickPosition: 'inline' },
+ },
+ },
+ chrome: { title: 'Revenue improved', subtitle: 'Monthly results' },
+};
+describe('measured y-axis headers', () => {
+ it.each([
+ 390, 1024, 1440,
+ ])('reserves a header band without changing the domain at %ipx', (width) => {
+ const old = compileChart(base, { width, height: 400 });
+ const next = compileChart(
+ {
+ ...base,
+ encoding: {
+ ...base.encoding,
+ y: {
+ ...base.encoding.y,
+ axis: { ...base.encoding.y.axis, titleAngle: 0, titleAnchor: 'end' },
+ },
+ },
+ },
+ { width, height: 400 },
+ );
+ expect(next.area.y).toBeGreaterThan(old.area.y);
+ expect(next.area.x).toBeLessThan(old.area.x);
+ expect(next.axes.y?.titlePosition?.angle).toBe(0);
+ expect(next.axes.y?.titlePosition?.y).toBeLessThan(next.area.y);
+ expect(next.axes.y?.ticks.map((t) => t.value)).toEqual(old.axes.y?.ticks.map((t) => t.value));
+ });
+});
+
+it.each([
+ { titleAngle: NaN },
+ { titleAngle: 'flat' },
+ { titleAnchor: 'sideways' },
+])('rejects invalid title placement %#', (axis) => {
+ expect(() =>
+ compileChart(
+ {
+ ...base,
+ encoding: {
+ ...base.encoding,
+ y: { ...base.encoding.y, axis: { title: 'Revenue', ...axis } },
+ },
+ },
+ { width: 640, height: 400 },
+ ),
+ ).toThrow('/encoding/y/axis/title');
+});
+
+it.each([
+ 0, 45, 90,
+])('reserves authored title angle %i without clipping its measured footprint', (angle) => {
+ const layout = compileChart(
+ {
+ ...base,
+ encoding: {
+ ...base.encoding,
+ y: {
+ ...base.encoding.y,
+ axis: { title: 'Revenue per customer ($)', titleAngle: angle, titleAnchor: 'middle' },
+ },
+ },
+ },
+ { width: 640, height: 400 },
+ );
+ const pos = layout.axes.y!.titlePosition!;
+ expect(pos.angle).toBe(angle);
+ expect(pos.x).toBeGreaterThan(0);
+ expect(pos.x).toBeLessThan(layout.area.x);
+ expect(Number.isFinite(layout.area.width)).toBe(true);
+});
+
+it('keeps mixed-unit headers above both axes in a shared dual-axis frame', () => {
+ const leaf = (title: string) => ({
+ ...base,
+ encoding: {
+ ...base.encoding,
+ y: { ...base.encoding.y, axis: { title, titleAngle: 0, titleAnchor: 'end' } },
+ },
+ });
+ const layout = compileLayer(
+ {
+ layer: [leaf('Revenue ($)'), leaf('Conversion (%)')],
+ resolve: { scale: { y: 'independent' } },
+ } as LayerSpec,
+ { width: 640, height: 400 },
+ );
+ expect(layout.axes.y?.titlePosition?.angle).toBe(0);
+ expect(layout.axes.y2?.titlePosition?.angle).toBe(0);
+ expect(layout.axes.y2?.titlePosition?.textAnchor).toBe('end');
+ expect(layout.axes.y2?.titlePosition?.y).toBeLessThan(layout.area.y);
+});
+
+it('keeps a large rotated title baseline far enough inside the viewport for its glyph ascent', () => {
+ const layout = compileChart(
+ { ...base, theme: { fonts: { sizes: { body: 26, axisTick: 24 } } } },
+ { width: 390, height: 960 },
+ );
+ expect(layout.axes.y!.titlePosition!.x).toBeGreaterThanOrEqual(26);
+});
diff --git a/packages/engine/src/__tests__/compile-animation.test.ts b/packages/engine/src/__tests__/compile-animation.test.ts
index bf67d856..54454082 100644
--- a/packages/engine/src/__tests__/compile-animation.test.ts
+++ b/packages/engine/src/__tests__/compile-animation.test.ts
@@ -102,10 +102,14 @@ describe('compileChart with animation', () => {
const indices = rectMarks.map(
(m) => (m as unknown as { animationIndex?: number }).animationIndex,
);
- for (const idx of indices) {
- expect(idx).toBeDefined();
- expect(typeof idx).toBe('number');
- }
+ // Value order is ascending: Q1 (100) first, then Q3 (200), then Q2 (300).
+ const byCategory = Object.fromEntries(
+ rectMarks.map((m) => [
+ (m as { data: Record }).data.category,
+ (m as unknown as { animationIndex?: number }).animationIndex,
+ ]),
+ );
+ expect(byCategory).toEqual({ Q1: 0, Q3: 1, Q2: 2 });
// Indices should be unique and sequential (0, 1, 2)
const sorted = [...indices].sort((a, b) => a! - b!);
@@ -151,8 +155,8 @@ describe('compileChart with animation', () => {
},
} as ChartSpec;
const layoutCompact = compileChart(specNoAnim, { width: 350, height: 400 });
- expect(layoutCompact.animation).toBeDefined();
- expect(layoutCompact.animation!.enter).toBeDefined();
+ // `animation: true` resolves the default entrance.
+ expect(layoutCompact.animation!.enter!.duration).toBe(450);
// At full width (> 700), no override applies, so no animation
const layoutFull = compileChart(specNoAnim, { width: 800, height: 400 });
diff --git a/packages/engine/src/__tests__/compile-chart.test.ts b/packages/engine/src/__tests__/compile-chart.test.ts
index f718ab34..117297a7 100644
--- a/packages/engine/src/__tests__/compile-chart.test.ts
+++ b/packages/engine/src/__tests__/compile-chart.test.ts
@@ -77,8 +77,8 @@ describe('compileChart', () => {
expect(layout.a11y.altText.length).toBeGreaterThan(0);
expect(layout.a11y.role).toBe('img');
- // Theme is resolved with actual color values
- expect(layout.theme.colors.background).toBeTruthy();
+ // Theme is resolved with actual color values (transparent so the host shows through)
+ expect(layout.theme.colors.background).toBe('transparent');
expect(layout.theme.colors.categorical.length).toBeGreaterThan(0);
// Dimensions match the input
@@ -138,7 +138,8 @@ describe('compileChart', () => {
expect(xTicks.length).toBeGreaterThan(0);
for (const tick of xTicks) {
expect(tick.position).toBeGreaterThanOrEqual(0);
- expect(tick.label).toBeTruthy();
+ // Data spans 2020-01-01..2021-01-01, so every temporal label names one of those years.
+ expect(tick.label).toMatch(/\b202[01]\b/);
}
// Y axis
@@ -146,7 +147,7 @@ describe('compileChart', () => {
expect(yTicks.length).toBeGreaterThan(0);
for (const tick of yTicks) {
expect(tick.position).toBeGreaterThanOrEqual(0);
- expect(tick.label).toBeTruthy();
+ expect(tick.label).toBe(String(tick.value));
}
// Axes have start and end points
@@ -165,10 +166,19 @@ describe('compileChart', () => {
});
it('legend entries have colors and shapes', () => {
- const layout = compileChart(lineSpec, { width: 600, height: 400 });
+ // Endpoint labels replace the legend by default, so turn them off to get entries.
+ const layout = compileChart(
+ { ...lineSpec, endpointLabels: false },
+ { width: 600, height: 400 },
+ );
+ expect(layout.legend.entries.map((e) => e.label)).toEqual(['US', 'UK']);
+ const strokes = new Map(
+ layout.marks.flatMap((m) => (m.type === 'line' ? [[m.seriesKey, m.stroke] as const] : [])),
+ );
for (const entry of layout.legend.entries) {
- expect(entry.color).toBeTruthy();
- expect(['circle', 'square', 'line']).toContain(entry.shape);
+ // Swatch color is the series' line color; line series get line swatches.
+ expect(entry.color).toBe(strokes.get(entry.label));
+ expect(entry.shape).toBe('line');
}
});
@@ -231,7 +241,7 @@ describe('compileChart', () => {
for (const mark of lineMarks) {
if (mark.type === 'line') {
expect(mark.points.length).toBeGreaterThan(0);
- expect(mark.stroke).toBeTruthy();
+ expect(mark.stroke).toMatch(/^#[0-9a-f]{6}$/i);
expect(mark.strokeWidth).toBeGreaterThan(0);
expect(mark.dataPoints).toBeDefined();
expect(mark.dataPoints!.length).toBeGreaterThan(0);
@@ -368,9 +378,11 @@ describe('compileChart', () => {
if (mark.type === 'rect') {
expect(mark.width).toBeGreaterThan(0);
expect(mark.height).toBeGreaterThan(0);
- expect(mark.fill).toBeTruthy();
+ expect(mark.fill).toMatch(/^#[0-9a-f]{6}$/i);
}
}
+ // Single series: every bar shares one fill.
+ expect(new Set(layout.marks.map((m) => (m.type === 'rect' ? m.fill : null))).size).toBe(1);
// Both axes should be present with ticks
expect(layout.axes.x!.ticks.length).toBeGreaterThan(0);
@@ -667,9 +679,9 @@ describe('compileChart', () => {
expect(layout.watermark).toBe(true);
});
- it('cramped height ignores the compile-option watermark fallback', () => {
+ it('cramped height respects the compile-option watermark override', () => {
const layout = compileChart(lineSpec, { width: 600, height: 180, watermark: true });
- expect(layout.watermark).toBe(false);
+ expect(layout.watermark).toBe(true);
});
it('explicit watermark: true at minimal height reserves the brand band', () => {
@@ -788,7 +800,8 @@ describe('compileChart', () => {
},
{ width: 400, height: 200 },
);
- expect(layout.axes.x).toBeDefined();
+ expect(layout.axes.x!.label).toBe('date');
+ expect(layout.axes.x!.ticks.length).toBeGreaterThan(0);
});
it('sparkline + explicit chrome.title still renders chrome', () => {
@@ -1110,7 +1123,7 @@ describe('compileChart', () => {
};
const layout = compileChart(spec, { width: 320, height: 200 });
expect(layout.display).toBe('sparkline');
- expect(layout.animation?.enter).toBeDefined();
+ expect(layout.animation?.enter?.duration).toBe(1100);
});
it('breakpoint chrome wins when top-level is sparkline', () => {
@@ -1165,7 +1178,7 @@ describe('compileChart', () => {
},
};
const layoutFull = compileChart(spec, { width: 1200, height: 600 });
- expect(layoutFull.axes.x).toBeDefined();
+ expect(layoutFull.axes.x!.label).toBe('date');
const layoutCompact = compileChart(spec, { width: 320, height: 200 });
expect(layoutCompact.axes.x).toBeUndefined();
});
@@ -1253,7 +1266,8 @@ describe('compileTable', () => {
// Columns are resolved with labels and widths
expect(layout.columns).toHaveLength(2);
expect(layout.columns[0].key).toBe('name');
- expect(layout.columns[0].label).toBeTruthy();
+ // No authored label: the column key is the header.
+ expect(layout.columns[0].label).toBe('name');
expect(layout.columns[0].width).toBeGreaterThan(0);
expect(layout.columns[1].key).toBe('age');
@@ -1268,7 +1282,7 @@ describe('compileTable', () => {
// Theme is resolved
expect(layout.theme.isDark).toBe(false);
- expect(layout.theme.colors.background).toBeTruthy();
+ expect(layout.theme.colors.background).toBe('transparent');
});
it('throws if a chart spec is passed', () => {
diff --git a/packages/engine/src/__tests__/compile-footnotes.test.ts b/packages/engine/src/__tests__/compile-footnotes.test.ts
index 94c164d5..3fb288eb 100644
--- a/packages/engine/src/__tests__/compile-footnotes.test.ts
+++ b/packages/engine/src/__tests__/compile-footnotes.test.ts
@@ -54,7 +54,10 @@ describe('auto-thinning footnote reserve', () => {
});
it('reserves bottom space so the footnote list clears the footer row', () => {
- const withFootnotes = compileChart(crowdedSpec, { width: 200, height: 340 });
+ const withFootnotes = compileChart(
+ { ...crowdedSpec, chromeLayout: 'subtract' },
+ { width: 200, height: 340 },
+ );
const footnotes = withFootnotes.chrome.footnotes ?? [];
expect(footnotes.length).toBeGreaterThan(0);
@@ -149,7 +152,8 @@ describe('auto-thinning footnote reserve', () => {
const baseline = compileChart({ ...spec, annotations: [] }, options);
const reserved = compileChart(spec, options);
expect(reserved.chrome.noteBandHeight).toBeGreaterThan(0);
- expect(reserved.area).toEqual(baseline.area);
+ for (const key of ['x', 'y', 'width', 'height'] as const)
+ expect(reserved.area[key]).toBeCloseTo(baseline.area[key], 8);
expect(reserved.dimensions.height - baseline.dimensions.height).toBeCloseTo(
reserved.chrome.noteBandHeight!,
8,
@@ -316,7 +320,7 @@ describe('auto-thinning in faceted charts', () => {
it.each([false, true])('grows facet chrome once with spec-level grow=%s', (onSpec) => {
const options = { width: 640, height: 600 };
- const baseline = compileChart(facetedSpec, options);
+ const baseline = compileChart({ ...facetedSpec, chromeLayout: 'subtract' }, options);
const grown = compileChart(
onSpec ? { ...facetedSpec, chromeLayout: 'grow' } : facetedSpec,
onSpec ? options : { ...options, chromeLayout: 'grow' },
@@ -393,18 +397,27 @@ describe('auto-thinning in faceted charts', () => {
});
it('reserves a band for the footnote list below the grid', () => {
- const withFootnotes = compileChart(facetedSpec, { width: 640, height: 400 });
+ const withFootnotes = compileChart(
+ { ...facetedSpec, chromeLayout: 'subtract' },
+ { width: 640, height: 400 },
+ );
const footnotes = withFootnotes.chrome.footnotes ?? [];
expect(footnotes.length).toBeGreaterThan(0);
const { annotations: _dropped, ...noAnnotations } = facetedSpec;
- const baseline = compileChart(noAnnotations as ChartSpec, { width: 640, height: 400 });
+ const baseline = compileChart({ ...noAnnotations, chromeLayout: 'subtract' } as ChartSpec, {
+ width: 640,
+ height: 400,
+ });
const plotBottom = (l: typeof withFootnotes) => l.area.y + l.area.height;
const pulledUpBy = plotBottom(baseline) - plotBottom(withFootnotes);
const band = footnotes.length * withFootnotes.theme.fonts.sizes.small * 1.3 + 4;
- expect(pulledUpBy).toBeGreaterThanOrEqual(band - 1e-9);
+ // Short facet panels retain a minimum plot budget. If it raises the figure
+ // height, that growth contributes to the reserved band as well.
+ const minimumPanelGrowth = withFootnotes.dimensions.height - baseline.dimensions.height;
+ expect(pulledUpBy + minimumPanelGrowth).toBeGreaterThanOrEqual(band - 1e-9);
});
// Regression: footnote numbers were keyed on the annotation's position in the
diff --git a/packages/engine/src/__tests__/compile-reentry.test.ts b/packages/engine/src/__tests__/compile-reentry.test.ts
new file mode 100644
index 00000000..15bcc2ba
--- /dev/null
+++ b/packages/engine/src/__tests__/compile-reentry.test.ts
@@ -0,0 +1,204 @@
+import type { ChartSpec, LayerSpec } from '@opendata-ai/openchart-core';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+// Count compile entries through the validate/normalize stage every chart
+// compile passes exactly once. A re-entered compile shows up as a second call.
+// Pass-through wrapper: a re-entered compile returns the same layout, so the
+// entry count is the only direct signal for this single-pass invariant.
+const entries = vi.hoisted(() => ({ count: 0 }));
+vi.mock('../compiler/index', async (importOriginal) => {
+ const original = await importOriginal();
+ return {
+ ...original,
+ compile: (...args: Parameters) => {
+ entries.count++;
+ return original.compile(...args);
+ },
+ };
+});
+
+const { compileChart, compileLayer } = await import('../compile');
+
+const annotated: ChartSpec = {
+ mark: 'line',
+ data: [
+ { x: 0, y: 1 },
+ { x: 1, y: 3 },
+ { x: 2, y: 2 },
+ ],
+ encoding: { x: { field: 'x', type: 'ordinal' }, y: { field: 'y', type: 'quantitative' } },
+ chrome: { title: 'Annotated', source: 'Source' },
+ annotations: [
+ { type: 'text', x: 1, y: 3, text: 'Peak' },
+ { type: 'text', x: 2, y: 2, text: 'Settles' },
+ ],
+};
+
+const shortFacet: ChartSpec = {
+ mark: 'point',
+ data: ['A', 'B', 'C', 'D'].flatMap((group) => [
+ { x: 0, y: 0, group },
+ { x: 10, y: 10, group },
+ ]),
+ encoding: {
+ x: { field: 'x', type: 'quantitative' },
+ y: { field: 'y', type: 'quantitative' },
+ facet: { field: 'group', type: 'nominal', columns: 1 },
+ },
+ chrome: { title: 'Short facet', source: 'Source' },
+ annotations: [
+ { type: 'text', x: 3, y: 3, text: 'First' },
+ { type: 'text', x: 6, y: 6, text: 'Second' },
+ ],
+};
+
+const dualAxis: LayerSpec = {
+ resolve: { scale: { y: 'independent' } },
+ endpointLabels: { content: 'label' },
+ annotations: [
+ { type: 'text', x: 'Jan', y: 120, text: 'Bars start' },
+ { type: 'text', x: 'Feb', y: 140, text: 'Bars end' },
+ ],
+ layer: [
+ {
+ mark: 'bar',
+ data: [
+ { x: 'Jan', y: 100 },
+ { x: 'Feb', y: 150 },
+ ],
+ encoding: { x: { field: 'x', type: 'ordinal' }, y: { field: 'y', type: 'quantitative' } },
+ },
+ {
+ mark: 'line',
+ data: [
+ { x: 'Jan', y: 2, s: 'Alpha' },
+ { x: 'Feb', y: 5, s: 'Alpha' },
+ { x: 'Jan', y: 6, s: 'Beta' },
+ { x: 'Feb', y: 4, s: 'Beta' },
+ ],
+ encoding: {
+ x: { field: 'x', type: 'ordinal' },
+ y: { field: 'y', type: 'quantitative' },
+ color: { field: 's', type: 'nominal' },
+ },
+ },
+ ],
+};
+
+beforeEach(() => {
+ entries.count = 0;
+});
+
+/** Annotated chart that also trips one sugar and one normalize warning. */
+const warned = {
+ ...annotated,
+ $schema: 'https://vega.github.io/schema/vega-lite/v5.json',
+ encoding: { ...annotated.encoding, y: { field: 'y' } },
+} as unknown as ChartSpec;
+
+describe('single-pass compile', () => {
+ it('enters compile once for an annotated chart', () => {
+ const layout = compileChart(annotated, { width: 600, height: 400 });
+ expect(layout.chrome.noteBandHeight).toBeGreaterThan(0);
+ expect(entries.count).toBe(1);
+ });
+
+ it('re-solves a short facet at its grown height inside one compile', () => {
+ const layout = compileChart(shortFacet, { width: 390, height: 200 });
+ expect(layout.chrome.grownHeight).toBeGreaterThan(0);
+ expect(entries.count).toBe(1);
+ });
+
+ it('compiles each dual-axis leaf once to measure and once frozen', () => {
+ const layout = compileLayer(dualAxis, { width: 640, height: 400 });
+ expect(layout.endpointLabels?.entries.length).toBeGreaterThan(0);
+ expect(entries.count).toBe(4);
+ });
+
+ it('emits dual-axis leaf warnings once', () => {
+ const warnings: string[] = [];
+ const [bars, lines] = dualAxis.layer as ChartSpec[];
+ compileLayer(
+ {
+ ...dualAxis,
+ layer: [
+ { ...bars, encoding: { ...bars.encoding, y: { field: 'y' } } } as ChartSpec,
+ { ...lines, encoding: { ...lines.encoding, y: { field: 'y' } } } as ChartSpec,
+ ],
+ },
+ { width: 640, height: 400, onWarn: (m) => warnings.push(m) },
+ );
+ // One per leaf: the primary (bars) and the secondary (lines) each infer y.
+ expect(warnings.filter((m) => m.includes('Inferred encoding.y.type'))).toHaveLength(2);
+ });
+
+ it('emits sugar and normalize warnings once for an annotated chart', () => {
+ const warnings: string[] = [];
+ compileChart(warned, { width: 600, height: 400, onWarn: (m) => warnings.push(m) });
+ expect(warnings.filter((m) => m.includes('$schema'))).toHaveLength(1);
+ expect(warnings.filter((m) => m.includes('Inferred encoding.y.type'))).toHaveLength(1);
+ });
+});
+
+describe('note overflow without re-entry', () => {
+ // A host measure whose wrapping is not monotone in the note ordinal: note 2
+ // wraps far wider than the widest-ordinal capacity predicted.
+ const measureText = (text: string, fontSize: number) => ({
+ width: text.startsWith('2. ') && text.length > 3 ? 10_000 : text.length * fontSize * 0.55,
+ height: fontSize,
+ });
+ const crowded: ChartSpec = {
+ ...annotated,
+ data: Array.from({ length: 8 }, (_, x) => ({ x, y: x % 3 })),
+ annotations: Array.from({ length: 6 }, (_, x) => ({
+ type: 'text' as const,
+ x,
+ y: x % 3,
+ text: `Callout number ${x} with words`,
+ })),
+ };
+
+ it('grows the figure by the overflow when notes sit outside the budget', () => {
+ const layout = compileChart(crowded, { width: 200, height: 340, measureText });
+ expect(entries.count).toBe(1);
+ expect(layout.chrome.footnotes?.length ?? 0).toBeGreaterThan(1);
+ const band = layout.chrome.noteBandHeight!;
+ const monotone = compileChart(crowded, {
+ width: 200,
+ height: 340,
+ measureText: (text, fontSize) => ({ width: text.length * fontSize * 0.55, height: fontSize }),
+ });
+ // The overflow grew only the figure; the plot stayed where the reserve put it.
+ expect(band).toBeGreaterThan(monotone.chrome.noteBandHeight!);
+ expect(layout.area).toEqual(monotone.area);
+ expect(layout.dimensions.height).toBeCloseTo(340 + band, 8);
+ expect(layout.chrome.grownHeight).toBeCloseTo(band, 8);
+ });
+
+ it('keeps a subtract figure fixed and reports the overflow', () => {
+ const diagnostics: string[] = [];
+ const layout = compileChart(
+ { ...crowded, chromeLayout: 'subtract' },
+ { width: 200, height: 340, measureText, onDiagnostic: (d) => diagnostics.push(d.code) },
+ );
+ expect(entries.count).toBe(1);
+ expect(layout.dimensions.height).toBe(340);
+ expect(layout.area.height).toBeGreaterThan(0);
+ expect(diagnostics).toContain('layout.note-capacity');
+ });
+
+ it('reports a subtract layer overflow once', () => {
+ const { annotations, ...leaf } = crowded;
+ const diagnostics: string[] = [];
+ const layout = compileLayer({ layer: [leaf], annotations } as LayerSpec, {
+ width: 200,
+ height: 340,
+ measureText,
+ chromeLayout: 'subtract',
+ onDiagnostic: (d) => diagnostics.push(d.code),
+ });
+ expect(layout.dimensions.height).toBe(340);
+ expect(layout.area.height).toBeGreaterThan(0);
+ expect(diagnostics.filter((code) => code === 'layout.note-capacity')).toHaveLength(1);
+ });
+});
diff --git a/packages/engine/src/__tests__/compile-snapshot.test.ts b/packages/engine/src/__tests__/compile-snapshot.test.ts
index 624058b6..b4404f96 100644
--- a/packages/engine/src/__tests__/compile-snapshot.test.ts
+++ b/packages/engine/src/__tests__/compile-snapshot.test.ts
@@ -1,10 +1,10 @@
/**
- * Integration snapshot test: the backstop for Step 7 refactor.
+ * Integration snapshots for the complete compiled chart contract.
*
* Compiles three representative ChartSpecs that exercise the extracted paths
* (legend-heavy, clipped-domain, watermark + gradient) and snapshots the full
- * ChartLayout. Pre-refactor snapshots are captured before helper extraction;
- * post-refactor snapshots must match exactly without --update-snapshots.
+ * ChartLayout. These began as the Step 7 extraction oracle; subsequent intended
+ * contract changes are reviewed alongside focused semantic and visual tests.
*
* Non-serializable fields (Map, Function) are normalized into plain structures
* so the snapshot can round-trip through the default serializer.
@@ -153,4 +153,48 @@ describe('compileChart snapshot (Step 7 oracle)', () => {
const layout = compileChart(spec, { width: 600, height: 400 });
expect(serializeLayout(layout)).toMatchSnapshot();
});
+
+ it('annotated scatter with auto, pinned, reference and range annotations', () => {
+ const spec = {
+ mark: 'point' as const,
+ data: Array.from({ length: 12 }, (_, i) => ({ x: i * 10, y: (i * 37) % 100 })),
+ encoding: {
+ x: { field: 'x', type: 'quantitative' as const },
+ y: { field: 'y', type: 'quantitative' as const },
+ },
+ chrome: { title: 'Annotated scatter' },
+ annotations: [
+ { id: 'auto', type: 'text' as const, x: 30, y: 11, text: 'Automatic callout', priority: 1 },
+ {
+ id: 'pinned',
+ type: 'text' as const,
+ x: 70,
+ y: 59,
+ text: 'Pinned callout',
+ subtitle: 'with a subtitle',
+ connector: 'curve' as const,
+ offset: { dx: -60, dy: -50 },
+ },
+ {
+ id: 'target',
+ type: 'refline' as const,
+ y: 50,
+ label: 'Target',
+ labelPlacement: 'outside' as const,
+ },
+ {
+ id: 'event',
+ type: 'refline' as const,
+ x: 40,
+ label: 'Policy change',
+ labelPlacement: 'outside' as const,
+ },
+ { id: 'band', type: 'range' as const, x1: 80, x2: 100, label: 'Forecast' },
+ ],
+ watermark: false,
+ };
+
+ const layout = compileChart(spec, { width: 640, height: 420 });
+ expect(serializeLayout(layout)).toMatchSnapshot();
+ });
});
diff --git a/packages/engine/src/__tests__/date-only-temporal.test.ts b/packages/engine/src/__tests__/date-only-temporal.test.ts
new file mode 100644
index 00000000..70514fe6
--- /dev/null
+++ b/packages/engine/src/__tests__/date-only-temporal.test.ts
@@ -0,0 +1,76 @@
+/**
+ * Date-only ISO strings ('2020', '2020-04', '2020-04-01') parse as UTC
+ * midnight, so their axis must be built in UTC too. A local-time scale put the
+ * ticks at local midnight: west of UTC the domain ended six hours before the
+ * last year's tick and dropped it, east of UTC the first tick fell off instead,
+ * and the labels changed with the machine's timezone.
+ */
+
+import { afterAll, beforeAll, describe, expect, it } from 'vitest';
+import { compileChart } from '../compile';
+
+const yearSpan = {
+ mark: 'line',
+ data: [
+ { date: '2020-01-01', value: 10 },
+ { date: '2020-07-01', value: 25 },
+ { date: '2021-01-01', value: 40 },
+ ],
+ encoding: {
+ x: { field: 'date', type: 'temporal' },
+ y: { field: 'value', type: 'quantitative' },
+ },
+};
+
+const isUtcMidnight = (d: Date) =>
+ d.getUTCHours() === 0 && d.getUTCMinutes() === 0 && d.getUTCSeconds() === 0;
+
+// CI runs in UTC, where a local-time scale is indistinguishable from a UTC
+// one; pin zones on both sides of it. Node re-reads TZ at runtime.
+describe.each(['America/Los_Angeles', 'Asia/Tokyo'])('date-only temporal data in %s', (zone) => {
+ const originalTz = process.env.TZ;
+ beforeAll(() => {
+ process.env.TZ = zone;
+ });
+ afterAll(() => {
+ process.env.TZ = originalTz;
+ });
+
+ it('ticks the first and last date of the data in any timezone', () => {
+ const layout = compileChart(structuredClone(yearSpan), { width: 600, height: 400 });
+ const ticks = layout.axes.x!.ticks;
+ const values = ticks.map((t) => (t.value as Date).toISOString());
+ expect(values[0]).toBe('2020-01-01T00:00:00.000Z');
+ expect(values.at(-1)).toBe('2021-01-01T00:00:00.000Z');
+ expect(ticks.every((t) => isUtcMidnight(t.value as Date))).toBe(true);
+ expect(ticks.at(-1)!.label).toBe('2021');
+ });
+
+ it('puts each point on the tick for its date', () => {
+ const layout = compileChart(structuredClone(yearSpan), { width: 600, height: 400 });
+ const [first, last] = [layout.axes.x!.ticks[0], layout.axes.x!.ticks.at(-1)!];
+ const line = layout.marks.find((m) => m.type === 'line') as { points: { x: number }[] };
+ expect(line.points[0].x).toBeCloseTo(first.position, 6);
+ expect(line.points.at(-1)!.x).toBeCloseTo(last.position, 6);
+ });
+
+ it('positions an annotation on a date-only axis', () => {
+ const layout = compileChart(
+ {
+ ...structuredClone(yearSpan),
+ annotations: [{ type: 'refline', x: '2020-07-01', label: 'Midyear' }],
+ },
+ { width: 600, height: 400 },
+ );
+ const line = layout.marks.find((m) => m.type === 'line') as { points: { x: number }[] };
+ const refline = layout.annotations[0] as { line?: { start: { x: number } } };
+ expect(refline.line?.start.x).toBeCloseTo(line.points[1].x, 6);
+ });
+
+ it('keeps a local-time axis for timestamps with a time of day', () => {
+ const spec = structuredClone(yearSpan);
+ spec.data = spec.data.map((d) => ({ ...d, date: `${d.date}T00:00` }));
+ const layout = compileChart(spec, { width: 600, height: 400 });
+ expect(layout.xInvert?.scaleType).toBe('time');
+ });
+});
diff --git a/packages/engine/src/__tests__/design-label-placement.test.ts b/packages/engine/src/__tests__/design-label-placement.test.ts
new file mode 100644
index 00000000..b9c94ddb
--- /dev/null
+++ b/packages/engine/src/__tests__/design-label-placement.test.ts
@@ -0,0 +1,287 @@
+import type { ChartSpec, RectMark } from '@opendata-ai/openchart-core';
+import { describe, expect, it } from 'vitest';
+import { compileChart } from '../compile';
+
+const bars: ChartSpec = {
+ mark: 'bar',
+ data: [
+ { name: 'Alpha', value: 100 },
+ { name: 'Beta', value: -25 },
+ { name: 'Gamma', value: 0 },
+ ],
+ encoding: { x: { field: 'value', type: 'quantitative' }, y: { field: 'name', type: 'nominal' } },
+ labels: { density: 'all' },
+};
+describe('authored value placement through the shared frame', () => {
+ it('omission preserves adaptive layout', () => {
+ const a = compileChart(bars, { width: 680, height: 400 });
+ const b = compileChart(
+ { ...bars, labels: { density: 'all', placement: 'adaptive' } },
+ { width: 680, height: 400 },
+ );
+ expect(b.area).toEqual(a.area);
+ expect(b.marks).toEqual(a.marks);
+ });
+ it('reserves a measured column and aligns signed values outside the plot', () => {
+ const base = compileChart(bars, { width: 680, height: 400 });
+ const layout = compileChart(
+ { ...bars, labels: { density: 'all', placement: 'column' } },
+ { width: 680, height: 400 },
+ );
+ expect(layout.area.width).toBeLessThan(base.area.width);
+ const rects = layout.marks.filter((m): m is RectMark => m.type === 'rect');
+ expect(rects).toHaveLength(3);
+ const labels = rects.flatMap((mark) => (mark.label ? [mark.label] : []));
+ expect(labels).toHaveLength(3);
+ expect(new Set(labels.map((m) => m.x)).size).toBe(1);
+ expect(labels[0].x).toBeGreaterThan(layout.area.x + layout.area.width);
+ });
+ it.each([
+ 'circle',
+ 'range',
+ 'arc',
+ ] as const)('rejects placement on %s at the authored path', (mark) => {
+ expect(() =>
+ compileChart(
+ {
+ ...bars,
+ mark,
+ encoding:
+ mark === 'arc'
+ ? {
+ theta: { field: 'value', type: 'quantitative' },
+ color: { field: 'name', type: 'nominal' },
+ }
+ : mark === 'range'
+ ? { ...bars.encoding, x2: { field: 'value', type: 'quantitative' } }
+ : bars.encoding,
+ labels: { placement: 'inside' },
+ } as ChartSpec,
+ { width: 680, height: 400 },
+ ),
+ ).toThrow('/labels/placement');
+ });
+ it('rejects a value column on grouped bars', () => {
+ expect(() =>
+ compileChart(
+ {
+ ...bars,
+ data: [...bars.data, { name: 'Alpha', value: 20 }],
+ encoding: { ...bars.encoding, color: { field: 'name', type: 'nominal' } },
+ labels: { placement: 'column' },
+ },
+ { width: 680, height: 400 },
+ ),
+ ).toThrow('/labels/placement');
+ });
+ it('rejects stacked-band options on a line', () => {
+ expect(() =>
+ compileChart(
+ {
+ mark: 'line',
+ data: [{ x: 'a', y: 1 }],
+ encoding: { x: { field: 'x', type: 'ordinal' }, y: { field: 'y', type: 'quantitative' } },
+ endpointLabels: { anchor: 'band' },
+ },
+ { width: 680, height: 400 },
+ ),
+ ).toThrow('/endpointLabels/anchor');
+ });
+});
+
+for (const width of [330, 390, 680]) {
+ it(`outside negative columns keep their value lane above the x axis at ${width}px`, () => {
+ const layout = compileChart(
+ {
+ mark: 'bar',
+ data: [
+ { name: 'negative', value: -20 },
+ { name: 'positive', value: 30 },
+ ],
+ encoding: {
+ x: { field: 'name', type: 'nominal' },
+ y: { field: 'value', type: 'quantitative' },
+ },
+ labels: { placement: 'outside', density: 'all', format: '.1f' },
+ },
+ { width, height: 400 },
+ );
+ const rects = layout.marks.filter((mark): mark is RectMark => mark.type === 'rect');
+ const negative = rects.find((mark) => mark.data.value === -20)!;
+ expect(negative.label?.visible).toBe(true);
+ expect(negative.label!.y).toBeGreaterThan(negative.y + negative.height);
+ expect(negative.label!.y).toBeLessThan(layout.axes.x!.start.y);
+ expect(layout.axes.x!.start.y).toBeGreaterThan(layout.area.y + layout.area.height);
+ });
+}
+it('facets reserve equally sized value columns from the complete figure values', () => {
+ const spec: ChartSpec = {
+ ...bars,
+ data: [
+ { name: 'Alpha', value: 1, region: 'A' },
+ { name: 'Beta', value: 10000, region: 'B' },
+ ],
+ encoding: { ...bars.encoding, row: { field: 'region', type: 'nominal' } },
+ labels: { placement: 'column', density: 'all', format: '.1f' },
+ };
+ const layout = compileChart(spec, { width: 680, height: 600 });
+ const panels = layout.facet!.panels;
+ expect(panels).toHaveLength(2);
+ const labels = panels.map((panel) => {
+ const mark = panel.marks.find((mark): mark is RectMark => mark.type === 'rect')!;
+ return mark.label!;
+ });
+ expect(labels[0].x).toBe(labels[1].x);
+ expect(panels[0].area.width).toBe(panels[1].area.width);
+ for (let i = 0; i < panels.length; i++)
+ expect(labels[i].x).toBeGreaterThan(panels[i].area.x + panels[i].area.width);
+});
+it('faceted fallback chooses one mode and reports its authored path', () => {
+ const diagnostics: import('@opendata-ai/openchart-core').DesignDiagnostic[] = [];
+ const layout = compileChart(
+ {
+ ...bars,
+ data: [
+ { name: 'A', value: 1, region: 'A' },
+ { name: 'B', value: 2, region: 'B' },
+ ],
+ encoding: { ...bars.encoding, row: { field: 'region', type: 'nominal' } },
+ labels: { placement: 'column', format: '.20f' },
+ },
+ { width: 180, height: 500, onDiagnostic: (d) => diagnostics.push(d) },
+ );
+ expect(
+ diagnostics.some(
+ (d) => d.path === '/labels/placement' && d.code.startsWith('layout.value-labels.'),
+ ),
+ ).toBe(true);
+ expect(
+ layout.facet!.panels.every((panel) =>
+ panel.marks.every((mark) => mark.type !== 'rect' || !mark.label),
+ ),
+ ).toBe(true);
+});
+
+it('faceted headers reserve a lane per panel together with a value column', () => {
+ const spec: ChartSpec = {
+ ...bars,
+ data: [
+ { name: 'A', value: 1, region: 'A' },
+ { name: 'B', value: 10000, region: 'B' },
+ ],
+ encoding: {
+ ...bars.encoding,
+ y: {
+ field: 'name',
+ type: 'nominal',
+ axis: { title: 'Department header', titleAngle: 0, titleAnchor: 'end' },
+ },
+ row: { field: 'region', type: 'nominal' },
+ },
+ labels: { placement: 'column', density: 'all', format: '.1f' },
+ };
+ const layout = compileChart(spec, { width: 390, height: 600 });
+ for (const panel of layout.facet!.panels) {
+ expect(panel.axes.y?.titlePosition?.y).toBeLessThan(panel.area.y);
+ expect(panel.axes.y?.titlePosition?.y).toBeGreaterThan(panel.header.y);
+ const label = panel.marks.find((mark): mark is RectMark => mark.type === 'rect')!.label!;
+ expect(label.x).toBeGreaterThan(panel.area.x + panel.area.width);
+ expect(label.x).toBeLessThan(layout.dimensions.width);
+ }
+});
+
+it.each([680, 1024])('wrapped facet value columns stay within their own cells at %ipx', (width) => {
+ const layout = compileChart(
+ {
+ ...bars,
+ data: ['A', 'B', 'C', 'D'].flatMap((region, i) => [
+ { name: 'First', value: (i + 1) * 1234.5, region },
+ { name: 'Second', value: -12.5, region },
+ ]),
+ encoding: { ...bars.encoding, facet: { field: 'region', type: 'nominal', columns: 2 } },
+ labels: { placement: 'column', density: 'all', format: ',.1f' },
+ },
+ { width, height: 700 },
+ );
+ const panels = layout.facet!.panels;
+ for (const panel of panels) {
+ const labels = panel.marks.flatMap((mark) =>
+ mark.type === 'rect' && mark.label ? [mark.label] : [],
+ );
+ expect(new Set(labels.map((label) => label.x)).size).toBe(1);
+ const rightNeighbor = panels.find(
+ (other) => Math.abs(other.area.y - panel.area.y) < 1 && other.area.x > panel.area.x,
+ );
+ for (const label of labels) {
+ expect(label.x).toBeGreaterThan(panel.area.x + panel.area.width);
+ expect(label.x).toBeLessThan(rightNeighbor?.area.x ?? width);
+ }
+ }
+});
+
+it('allows category-colored simple bars in a value column', () => {
+ const layout = compileChart(
+ {
+ ...bars,
+ encoding: { ...bars.encoding, color: { field: 'name', type: 'nominal' } },
+ labels: { placement: 'column' },
+ },
+ { width: 640, height: 400 },
+ );
+ expect(layout.marks.filter((m) => m.type === 'rect')).toHaveLength(3);
+});
+
+it('accepts an inferred nominal category for a simple value column', () => {
+ const layout = compileChart(
+ {
+ ...bars,
+ encoding: { x: { field: 'value' }, y: { field: 'name' } },
+ labels: { placement: 'column' },
+ },
+ { width: 680, height: 400 },
+ );
+ expect(layout.marks.filter((mark) => mark.type === 'rect').every((mark) => !!mark.label)).toBe(
+ true,
+ );
+});
+
+it('validates value-column cardinality after one aggregate transform', () => {
+ const layout = compileChart(
+ {
+ mark: 'bar',
+ data: [
+ { name: 'A', value: 10 },
+ { name: 'A', value: 20 },
+ { name: 'B', value: 3 },
+ ],
+ transform: [{ aggregate: [{ op: 'sum', field: 'value', as: 'total' }], groupby: ['name'] }],
+ encoding: {
+ x: { field: 'total', type: 'quantitative' },
+ y: { field: 'name', type: 'nominal' },
+ },
+ labels: { placement: 'column', density: 'all' },
+ },
+ { width: 680, height: 400 },
+ );
+ const rects = layout.marks.filter((mark): mark is RectMark => mark.type === 'rect');
+ expect(rects).toHaveLength(2);
+ expect(rects.map((mark) => mark.data.total)).toEqual([30, 3]);
+ expect(new Set(rects.map((mark) => mark.label?.x)).size).toBe(1);
+});
+
+it('still rejects genuinely grouped values after preparation at the authored placement path', () => {
+ expect(() =>
+ compileChart(
+ {
+ ...bars,
+ data: [
+ { name: 'A', value: 1, series: 'One' },
+ { name: 'A', value: 2, series: 'Two' },
+ ],
+ encoding: { ...bars.encoding, color: { field: 'series', type: 'nominal' } },
+ labels: { placement: 'column' },
+ },
+ { width: 680, height: 400 },
+ ),
+ ).toThrow('/labels/placement');
+});
diff --git a/packages/engine/src/__tests__/dimensions.test.ts b/packages/engine/src/__tests__/dimensions.test.ts
index 74db79b0..be988d41 100644
--- a/packages/engine/src/__tests__/dimensions.test.ts
+++ b/packages/engine/src/__tests__/dimensions.test.ts
@@ -738,7 +738,7 @@ describe('computeDimensions', () => {
const dims = computeDimensions(spec, { width: 600, height: 400 }, emptyLegend, lightTheme);
expect(dims.chromeStripped).toBeFalsy();
- expect(dims.chrome.title).toBeDefined();
+ expect(dims.chrome.title?.text).toBe('Short title');
});
it('does not trip the plot-share guardrail in grow mode', () => {
@@ -751,7 +751,7 @@ describe('computeDimensions', () => {
};
const dims = computeDimensions(spec, { width: 300, height: 360 }, emptyLegend, lightTheme);
- expect(dims.chrome.subtitle).toBeDefined();
+ expect(dims.chrome.subtitle?.text).toBe(plotEatingChrome.subtitle.text);
expect(dims.chromeStripped).toBeFalsy();
});
diff --git a/packages/engine/src/__tests__/facet-grow-opt-in.test.ts b/packages/engine/src/__tests__/facet-grow-opt-in.test.ts
index d69a4e72..dba28086 100644
--- a/packages/engine/src/__tests__/facet-grow-opt-in.test.ts
+++ b/packages/engine/src/__tests__/facet-grow-opt-in.test.ts
@@ -24,7 +24,10 @@ const spec: ChartSpec = {
};
describe('facet chrome growth', () => {
it('appends one chrome/note band while preserving the non-chrome plot budget', () => {
- const subtract = compileChart(spec, { width: 390, height: 650 });
+ const subtract = compileChart(
+ { ...spec, chromeLayout: 'subtract' },
+ { width: 390, height: 650 },
+ );
const grow = compileChart({ ...spec, chromeLayout: 'grow' }, { width: 390, height: 650 });
const added =
grow.chrome.topHeight + grow.chrome.bottomHeight + (grow.chrome.noteBandHeight ?? 0);
@@ -36,10 +39,15 @@ describe('facet chrome growth', () => {
grow.facet!.panels.every((panel) => panel.annotations.every((a) => a.footnoteIndex)),
).toBe(true);
});
- it('keeps default/subtract identical and accepts the compile option', () => {
+ it('grows only notes by default and accepts the compile option', () => {
const opts = { width: 800, height: 650 };
- expect(compileChart(spec, opts)).toEqual(
- compileChart({ ...spec, chromeLayout: 'subtract' }, opts),
+ const defaultLayout = compileChart(spec, opts);
+ const subtract = compileChart({ ...spec, chromeLayout: 'subtract' }, opts);
+ expect(defaultLayout.dimensions.height - subtract.dimensions.height).toBeCloseTo(
+ defaultLayout.chrome.noteBandHeight ?? 0,
+ );
+ expect(defaultLayout.chrome.grownHeight ?? 0).toBeCloseTo(
+ defaultLayout.chrome.noteBandHeight ?? 0,
);
expect(compileChart(spec, { ...opts, chromeLayout: 'grow' })).toEqual(
compileChart({ ...spec, chromeLayout: 'grow' }, opts),
diff --git a/packages/engine/src/__tests__/facet.test.ts b/packages/engine/src/__tests__/facet.test.ts
index 95f974a8..87a18e78 100644
--- a/packages/engine/src/__tests__/facet.test.ts
+++ b/packages/engine/src/__tests__/facet.test.ts
@@ -185,8 +185,9 @@ describe('compileChart with facet', () => {
expect(panel.marks.length).toBeGreaterThan(0);
expect(panel.area.width).toBeGreaterThan(0);
expect(panel.area.height).toBeGreaterThan(0);
- expect(panel.header.text).toBeTruthy();
}
+ // One header per facet value, sorted ascending as in Vega-Lite's facet default.
+ expect(layout.facet!.panels.map((p) => p.header.text)).toEqual(['DE', 'UK', 'US']);
});
it('figure-level axes are undefined when faceted', () => {
@@ -258,7 +259,8 @@ describe('compileChart with row faceting', () => {
it('keeps y-axis on all panels', () => {
const layout = compileChart(rowSpec, { width: 600, height: 600 });
for (const panel of layout.facet!.panels) {
- expect(panel.axes.y).toBeDefined();
+ // Each row panel labels its own country band.
+ expect(panel.axes.y!.ticks.map((t) => t.label)).toEqual(['US', 'UK', 'DE']);
}
});
@@ -364,9 +366,6 @@ describe('compileChart with column faceting', () => {
it('creates panels in a single row', () => {
const layout = compileChart(colSpec, { width: 800, height: 400 });
- for (const panel of layout.facet!.panels) {
- expect(panel.header.text).toBeTruthy();
- }
- expect(layout.facet!.panels).toHaveLength(2);
+ expect(layout.facet!.panels.map((p) => p.header.text)).toEqual(['2020', '2021']);
});
});
diff --git a/packages/engine/src/__tests__/figure-frame.test.ts b/packages/engine/src/__tests__/figure-frame.test.ts
new file mode 100644
index 00000000..6625fa2e
--- /dev/null
+++ b/packages/engine/src/__tests__/figure-frame.test.ts
@@ -0,0 +1,214 @@
+/**
+ * The layout plan and the figure budget build one figure frame (layout/frame.ts),
+ * so the plot the plan measures against is the plot the budget places. These
+ * specs each carry a reservation that once existed only on the budget side.
+ */
+
+import { describe, expect, it } from 'vitest';
+import { compileChartInternal } from '../compile/chart';
+import type { InternalCompileOptions, LeafContext } from '../compile/internal-options';
+
+const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'];
+const regions = ['North America', 'Western Europe', 'Southeast Asia'];
+const series = months.flatMap((month, i) =>
+ regions.map((region, r) => ({ month, region, value: 40 + i * 7 + r * 13 })),
+);
+
+const lineBase = {
+ mark: 'line',
+ data: series,
+ encoding: {
+ x: { field: 'month', type: 'ordinal' },
+ y: { field: 'value', type: 'quantitative' },
+ color: { field: 'region', type: 'nominal' },
+ },
+ chrome: { title: 'Regional output', subtitle: 'Index, first half of the year' },
+};
+
+const SPECS: Record> = {
+ 'bar value column': {
+ mark: 'bar',
+ data: [
+ { name: 'A long category name', value: -12.34 },
+ { name: 'Other', value: 0 },
+ { name: 'Third', value: 123.45 },
+ ],
+ encoding: {
+ x: { field: 'value', type: 'quantitative' },
+ y: { field: 'name', type: 'nominal' },
+ },
+ labels: { placement: 'column', format: ',.2f', suffix: ' million' },
+ },
+ 'outside signed columns': {
+ mark: 'bar',
+ data: [
+ { name: 'Negative', value: -12.34 },
+ { name: 'Zero', value: 0 },
+ { name: 'Positive', value: 123.45 },
+ ],
+ encoding: {
+ x: { field: 'name', type: 'nominal' },
+ y: { field: 'value', type: 'quantitative' },
+ },
+ labels: { placement: 'outside', format: ',.2f', suffix: ' million' },
+ },
+ 'value column with axis header': {
+ mark: 'bar',
+ data: [
+ { name: 'Negative', value: -12.34 },
+ { name: 'Zero', value: 0 },
+ { name: 'Positive', value: 123.45 },
+ ],
+ encoding: {
+ x: { field: 'value', type: 'quantitative' },
+ y: {
+ field: 'name',
+ type: 'nominal',
+ axis: {
+ title: 'Department with a deliberately long header',
+ titleAngle: 0,
+ titleAnchor: 'end',
+ },
+ },
+ },
+ labels: { placement: 'column', format: ',.2f' },
+ },
+ 'endpoint label column': { ...lineBase, endpointLabels: true },
+ 'right legend column': { ...lineBase, endpointLabels: false, legend: { position: 'right' } },
+ 'nominal x beside a right legend': {
+ mark: 'bar',
+ data: series,
+ encoding: {
+ x: { field: 'region', type: 'nominal' },
+ y: { field: 'value', type: 'quantitative', aggregate: 'sum' },
+ color: { field: 'month', type: 'nominal' },
+ },
+ legend: { position: 'right' },
+ },
+ 'size legend': {
+ mark: 'point',
+ data: series,
+ encoding: {
+ x: { field: 'month', type: 'ordinal' },
+ y: { field: 'value', type: 'quantitative' },
+ size: { field: 'value', type: 'quantitative' },
+ },
+ },
+ 'outside reference labels': {
+ ...lineBase,
+ annotations: [
+ { type: 'refline', x: 'Feb', label: 'Policy change', labelPlacement: 'outside' },
+ { type: 'refline', x: 'Mar', label: 'Supply shock', labelPlacement: 'outside' },
+ { type: 'refline', y: 70, label: 'Target', labelPlacement: 'outside' },
+ ],
+ },
+ 'metric bar': {
+ ...lineBase,
+ metrics: [
+ { label: 'Peak', value: '108' },
+ { label: 'Change', value: '+35%' },
+ ],
+ },
+ 'series summary note band': { ...lineBase, responsive: { labelSummary: true } },
+ 'y-axis title gutter': {
+ ...lineBase,
+ encoding: {
+ ...lineBase.encoding,
+ y: { field: 'value', type: 'quantitative', axis: { title: 'Output index' } },
+ },
+ },
+};
+
+const VIEWPORTS = [
+ { width: 340, height: 300 },
+ { width: 640, height: 400 },
+ { width: 1100, height: 520 },
+];
+
+// Cases whose chrome the guardrail strips, so the budget's plot outgrows the plan's.
+const STRIPPED = new Set(['metric bar @ 340x300']);
+
+function compileWithLeaf(spec: Record, options: InternalCompileOptions) {
+ const leafContext: LeafContext = {};
+ const layout = compileChartInternal(structuredClone(spec), { ...options, leafContext });
+ return { plan: leafContext.plan!, budget: leafContext.budget!, layout };
+}
+
+describe('figure frame: plan and budget place the same plot', () => {
+ for (const [name, spec] of Object.entries(SPECS)) {
+ for (const viewport of VIEWPORTS) {
+ it(`${name} @ ${viewport.width}x${viewport.height}`, () => {
+ const { plan, budget, layout } = compileWithLeaf(spec, viewport);
+ // The chrome ladder stepping down is the one sanctioned divergence: the
+ // compile then regenerates ticks against the final plot it draws.
+ const stripped = STRIPPED.has(`${name} @ ${viewport.width}x${viewport.height}`);
+ expect(!!budget.chromeStripped).toBe(stripped);
+ if (stripped) {
+ expect(layout.area).toEqual(budget.chartArea);
+ return;
+ }
+ expect(plan.plotArea).toEqual(budget.chartArea);
+ // The bottom margin was reserved for the x labels computeAxes draws:
+ // only band axes rotate, and at the plot width they are drawn on.
+ expect(plan.xAxisExtent).toBeCloseTo(layout.axes.x?.extent ?? 0, 1);
+ });
+ }
+ }
+
+ it('picks the x-label angle at the plot the y-axis title gutter leaves', () => {
+ // At this width flat labels fit the plot before the title widens the
+ // gutter, and not after.
+ const spec = {
+ mark: 'bar',
+ data: [
+ { category: 'Information Technology', value: 32.4 },
+ { category: 'Health Care Services', value: 13.1 },
+ { category: 'Financial Services', value: 12.8 },
+ { category: 'Consumer Discretionary', value: 10.5 },
+ { category: 'Communication Services', value: 8.9 },
+ { category: 'Industrial Manufacturing', value: 8.4 },
+ ],
+ encoding: {
+ x: { field: 'category', type: 'nominal' },
+ y: { field: 'value', type: 'quantitative', axis: { title: 'Weight (%)' } },
+ },
+ chrome: { title: 'S&P 500 Sector Weights', source: 'Source: S&P Dow Jones Indices' },
+ };
+ const { plan, layout } = compileWithLeaf(spec, { width: 1380, height: 400 });
+ expect(layout.axes.x?.tickAngle).toBe(-45);
+ expect(plan.xAxisExtent).toBeCloseTo(layout.axes.x?.extent ?? 0, 1);
+ });
+
+ it('picks the x-label angle at the plot inline y labels leave', () => {
+ // Inline y tick labels inset the bands computeAxes draws on; at this width
+ // flat labels fit the full plot but not the inset one.
+ const spec = {
+ mark: 'bar',
+ data: [
+ { category: 'Information Technology', value: 32.4 },
+ { category: 'Health Care Services', value: 13.1 },
+ { category: 'Financial Services', value: 12.8 },
+ { category: 'Consumer Discretionary', value: 10.5 },
+ { category: 'Communication Services', value: 8.9 },
+ { category: 'Industrial Manufacturing', value: 8.4 },
+ ],
+ encoding: {
+ x: { field: 'category', type: 'nominal' },
+ y: { field: 'value', type: 'quantitative', axis: { tickPosition: 'inline' } },
+ },
+ };
+ for (const width of [1350, 1360, 1370]) {
+ const { plan, layout } = compileWithLeaf(spec, { width, height: 400 });
+ expect(plan.xAxisExtent).toBeCloseTo(layout.axes.x?.extent ?? 0, 1);
+ }
+ });
+
+ it('a secondary y-axis reserve narrows the planned plot', () => {
+ const viewport = { width: 640, height: 400 };
+ const { plan, budget } = compileWithLeaf(SPECS['endpoint label column'], {
+ ...viewport,
+ rightAxisReserve: 72,
+ });
+ expect(plan.plotArea).toEqual(budget.chartArea);
+ });
+});
diff --git a/packages/engine/src/__tests__/grid-hierarchy.test.ts b/packages/engine/src/__tests__/grid-hierarchy.test.ts
new file mode 100644
index 00000000..f16089b4
--- /dev/null
+++ b/packages/engine/src/__tests__/grid-hierarchy.test.ts
@@ -0,0 +1,64 @@
+import { describe, expect, it } from 'vitest';
+import { compileChart } from '../compile';
+
+describe('authored zero gridline role', () => {
+ const base = {
+ mark: 'point',
+ data: [
+ { x: -1, y: -1 },
+ { x: 2, y: 2 },
+ ],
+ encoding: { x: { field: 'x', type: 'quantitative' }, y: { field: 'y', type: 'quantitative' } },
+ };
+ it('changes only an existing zero rule and leaves scales intact', () => {
+ const before = compileChart(base, { width: 640, height: 400 });
+ const after = compileChart(
+ { ...base, theme: { colors: { gridlineZero: '#778899' } } },
+ { width: 640, height: 400 },
+ );
+ expect(after.axes.y?.ticks).toEqual(before.axes.y?.ticks);
+ expect(after.area).toEqual(before.area);
+ const zero = after.axes.y!.ticks.find((t) => t.value === 0)!;
+ expect(after.axes.y!.gridlines.find((g) => g.position === zero.position)?.color).toBe(
+ '#778899',
+ );
+ expect(
+ after.axes
+ .y!.gridlines.filter((g) => g.position !== zero.position)
+ .every((g) => g.color === undefined),
+ ).toBe(true);
+ });
+ it('uses the default zero hierarchy only on existing enabled rules', () => {
+ const layout = compileChart(base, { width: 640, height: 400 });
+ const zero = layout.axes.y!.ticks.find((t) => t.value === 0)!;
+ expect(layout.axes.y!.gridlines.find((g) => g.position === zero.position)?.color).toBe(
+ 'rgba(0,0,0,0.22)',
+ );
+ const without = compileChart(
+ { ...base, encoding: { ...base.encoding, y: { ...base.encoding.y, axis: { grid: false } } } },
+ { width: 640, height: 400 },
+ );
+ expect(without.axes.y!.gridlines).toHaveLength(0);
+ expect(without.axes.y!.ticks).toEqual(layout.axes.y!.ticks);
+ expect(without.area).toEqual(layout.area);
+ });
+ it('does not add zero to a positive log domain', () => {
+ const chart = compileChart(
+ {
+ ...base,
+ data: [
+ { x: 2, y: 10 },
+ { x: 3, y: 100 },
+ ],
+ encoding: {
+ ...base.encoding,
+ y: { field: 'y', type: 'quantitative', scale: { type: 'log' } },
+ },
+ theme: { colors: { gridlineZero: '#778899' } },
+ },
+ { width: 640, height: 400 },
+ );
+ expect(chart.axes.y?.ticks.some((t) => t.value === 0)).toBe(false);
+ expect(chart.axes.y?.gridlines.every((g) => g.color === undefined)).toBe(true);
+ });
+});
diff --git a/packages/engine/src/__tests__/known-bugs.test.ts b/packages/engine/src/__tests__/known-bugs.test.ts
index dd5ddc6a..08bc539f 100644
--- a/packages/engine/src/__tests__/known-bugs.test.ts
+++ b/packages/engine/src/__tests__/known-bugs.test.ts
@@ -249,11 +249,8 @@ describe('known layout bugs', () => {
});
});
- // Regression: `resolveTextAnnotation` bolts the lede rule on (a `subtitle`
- // promotes the primary line to 700) BEFORE branching to the drop-line resolver,
- // which used to resolve no subtitle at all. Net effect: authoring a subtitle on
- // a drop-line silently threw the subtitle away but still bolded the primary --
- // a field whose only visible effect was a mystery weight change.
+ // Regression: drop-line resolution used to discard subtitle content. The v9
+ // regular-weight default must preserve the subtitle on this connector path.
describe('a drop-line annotation honors its subtitle', () => {
// Annotate Mar, not the Feb maximum: a callout on the chart's top value clamps
// against the chart edge and has nowhere to go, which is a separate (and
@@ -290,8 +287,8 @@ describe('known layout bugs', () => {
expect(annotation?.subtitle?.text).toBe('the highest month');
});
- test('still promotes the primary line to a bold lede', () => {
- expect(dropLineWithSubtitle()?.label?.style.fontWeight).toBe(700);
+ test('keeps the primary line regular by default', () => {
+ expect(dropLineWithSubtitle()?.label?.style.fontWeight).toBe(400);
});
test('renders the subtitle below the primary line, left edges aligned', () => {
@@ -529,7 +526,7 @@ describe('known layout bugs', () => {
return style;
};
- test('a subtitle promotes the primary line to a bold lede', () => {
+ test('a subtitle keeps the primary line regular by default', () => {
const style = styleOf({
type: 'text',
x: '2025',
@@ -537,7 +534,7 @@ describe('known layout bugs', () => {
text: 'Peak',
subtitle: 'the warmest year on record',
});
- expect(style.fontWeight).toBe(700);
+ expect(style.fontWeight).toBe(400);
});
test('a plain annotation stays regular weight', () => {
@@ -550,7 +547,7 @@ describe('known layout bugs', () => {
expect(style.fontSize).toBe(13);
});
- test('an explicit fontWeight beats the lede rule', () => {
+ test('an explicit fontWeight overrides the regular default', () => {
const style = styleOf({
type: 'text',
x: '2025',
diff --git a/packages/engine/src/__tests__/layer-endpoint-identification.test.ts b/packages/engine/src/__tests__/layer-endpoint-identification.test.ts
new file mode 100644
index 00000000..7f88bdf0
--- /dev/null
+++ b/packages/engine/src/__tests__/layer-endpoint-identification.test.ts
@@ -0,0 +1,141 @@
+import type { ChartSpec, DesignDiagnostic, LayerSpec } from '@opendata-ai/openchart-core';
+import { describe, expect, it } from 'vitest';
+import { compileChart, compileLayer } from '../compile';
+
+function area(names: string[]): ChartSpec {
+ return {
+ mark: 'area',
+ data: names.flatMap((series, index) =>
+ [1, 2, 3].map((x) => ({ x, series, value: 20 + index * 10 })),
+ ),
+ encoding: {
+ x: { field: 'x', type: 'quantitative' },
+ y: { field: 'value', type: 'quantitative' },
+ color: { field: 'series', type: 'nominal' },
+ },
+ endpointLabels: {
+ placement: 'interior',
+ anchor: 'band',
+ ends: 'both',
+ format: '.1f',
+ showLeader: false,
+ },
+ };
+}
+
+const spec: LayerSpec = {
+ layer: [area(['A', 'B']), area(['C', 'D'])],
+ resolve: { scale: { y: 'independent' } },
+};
+
+describe('independent area identification', () => {
+ it('keeps a solo area sparkline tight when no color grouping exists', () => {
+ const layout = compileChart(
+ {
+ mark: 'area',
+ display: 'sparkline',
+ data: [
+ { x: 1, value: 4000 },
+ { x: 2, value: 4010 },
+ ],
+ encoding: {
+ x: { field: 'x', type: 'quantitative' },
+ y: { field: 'value', type: 'quantitative' },
+ },
+ },
+ { width: 300, height: 100 },
+ );
+ const mark = layout.marks.find((item) => item.type === 'area');
+ expect(mark?.type).toBe('area');
+ if (mark?.type !== 'area') return;
+ const ys = mark.topPoints.map((point) => point.y);
+ expect(Math.max(...ys) - Math.min(...ys)).toBeGreaterThan(layout.area.height * 0.8);
+ });
+ it.each([
+ undefined,
+ 'zero',
+ 'normalize',
+ 'center',
+ ] as const)('retains the full stacked domain for %s offsets before anchoring labels', (stack) => {
+ const input: LayerSpec = {
+ ...spec,
+ layer: [area(['A', 'B']), area(['C', 'D'])].map((leaf) => ({
+ ...leaf,
+ encoding: { ...leaf.encoding, y: { ...leaf.encoding!.y!, stack } },
+ })),
+ };
+ const layout = compileLayer(input, { width: 1024, height: 500 });
+ for (const mark of layout.marks) {
+ if (mark.type !== 'area') continue;
+ for (const point of [...mark.topPoints, ...mark.bottomPoints]) {
+ expect(point.y).toBeGreaterThanOrEqual(layout.area.y - 1e-6);
+ expect(point.y).toBeLessThanOrEqual(layout.area.y + layout.area.height + 1e-6);
+ }
+ }
+ for (const entries of [layout.endpointLabels!.entries, layout.endpointLabels!.leading!]) {
+ const sorted = [...entries].sort((a, b) => a.labelY - b.labelY);
+ for (let index = 1; index < sorted.length; index++) {
+ expect(sorted[index].labelY - sorted[index - 1].labelY).toBeGreaterThanOrEqual(35);
+ }
+ }
+ });
+ it('retains every name and authored value format when independent interiors cannot be combined', () => {
+ const diagnostics: DesignDiagnostic[] = [];
+ const layout = compileLayer(spec, {
+ width: 1024,
+ height: 500,
+ onDiagnostic: (diagnostic) => diagnostics.push(diagnostic),
+ });
+ expect(layout.endpointLabels?.interior ?? []).toHaveLength(0);
+ for (const entries of [layout.endpointLabels!.entries, layout.endpointLabels!.leading!]) {
+ expect(entries.map((entry) => entry.seriesKey).sort()).toEqual(['A', 'B', 'C', 'D']);
+ for (const entry of entries) {
+ expect(entry.labelLines).toContain(entry.seriesKey);
+ expect(entry.value).toMatch(/^(20|30)\.0$/);
+ expect(entry.source?.observed).toBe(true);
+ }
+ }
+ expect(
+ diagnostics
+ .filter((item) => item.code === 'LAYER_INTERIOR_LABEL_FALLBACK')
+ .map((item) => item.path),
+ ).toEqual(['/resolve/scale/y', '/resolve/scale/y']);
+ expect(
+ spec.layer.every(
+ (leaf) =>
+ 'endpointLabels' in leaf &&
+ typeof leaf.endpointLabels === 'object' &&
+ leaf.endpointLabels?.placement === 'interior',
+ ),
+ ).toBe(true);
+ });
+
+ it('keeps required band leaders on both merged columns even when optional leaders are disabled', () => {
+ const layout = compileLayer(spec, { width: 1024, height: 500 });
+ for (const entries of [layout.endpointLabels!.entries, layout.endpointLabels!.leading!]) {
+ expect(entries).toHaveLength(4);
+ expect(entries.every((entry) => entry.showLeader)).toBe(true);
+ }
+ });
+
+ it('applies the fallback after resolving root endpoint settings over leaf settings', () => {
+ const rootAuthored: LayerSpec = {
+ ...spec,
+ layer: [area(['A', 'B']), area(['C', 'D'])].map((leaf) => ({
+ ...leaf,
+ endpointLabels: undefined,
+ })),
+ endpointLabels: { placement: 'interior', anchor: 'band', format: '.1f' },
+ };
+ const layout = compileLayer(rootAuthored, { width: 1024, height: 500 });
+ expect(layout.endpointLabels?.interior ?? []).toHaveLength(0);
+ expect(
+ layout.endpointLabels!.entries.map((entry) => entry.labelLines.join(' ')).sort(),
+ ).toEqual(['A', 'B', 'C', 'D']);
+ expect(rootAuthored.endpointLabels).toEqual({
+ placement: 'interior',
+ anchor: 'band',
+ format: '.1f',
+ });
+ });
+});
diff --git a/packages/engine/src/__tests__/layer-identity.test.ts b/packages/engine/src/__tests__/layer-identity.test.ts
index db166988..832b3166 100644
--- a/packages/engine/src/__tests__/layer-identity.test.ts
+++ b/packages/engine/src/__tests__/layer-identity.test.ts
@@ -71,11 +71,29 @@ describe('layered observation identity', () => {
expect(rows).toEqual([
{
name: 'Share',
- content: expect.objectContaining({ fields: [{ label: 'Share', value: '25%' }] }),
+ content: expect.objectContaining({
+ fields: [
+ {
+ label: 'Share',
+ value: '25%',
+ role: 'value',
+ source: { field: 'value', channel: 'tooltip', tooltipIndex: 0, layer: 'layer:1' },
+ },
+ ],
+ }),
},
{
name: 'Revenue',
- content: expect.objectContaining({ fields: [{ label: 'Revenue', value: '$1,250' }] }),
+ content: expect.objectContaining({
+ fields: [
+ {
+ label: 'Revenue',
+ value: '$1,250',
+ role: 'value',
+ source: { field: 'value', channel: 'tooltip', tooltipIndex: 0, layer: 'layer:0' },
+ },
+ ],
+ }),
},
]);
expect(layout.tooltipDescriptors.size).toBe(2);
@@ -132,8 +150,22 @@ describe('layered observation identity', () => {
expect(series).toHaveLength(2);
expect(series.map((item) => item.seriesKey)).toEqual(['Shared', 'Shared']);
expect(series.map((item) => item.dataPoints?.[0].tooltip?.fields)).toEqual([
- [{ label: 'Money', value: '$10' }],
- [{ label: 'Rate', value: '10%' }],
+ [
+ {
+ label: 'Money',
+ value: '$10',
+ role: 'value',
+ source: { field: 'value', channel: 'tooltip', tooltipIndex: 0, layer: 'layer:0' },
+ },
+ ],
+ [
+ {
+ label: 'Rate',
+ value: '10%',
+ role: 'value',
+ source: { field: 'value', channel: 'tooltip', tooltipIndex: 0, layer: 'layer:1' },
+ },
+ ],
]);
expect(series[0].pointKeys?.[0]).not.toBe(series[1].pointKeys?.[0]);
});
diff --git a/packages/engine/src/__tests__/layer-legend-assembly.test.ts b/packages/engine/src/__tests__/layer-legend-assembly.test.ts
new file mode 100644
index 00000000..beb8412d
--- /dev/null
+++ b/packages/engine/src/__tests__/layer-legend-assembly.test.ts
@@ -0,0 +1,154 @@
+import type { ChartSpec, LayerSpec, LegendConfig } from '@opendata-ai/openchart-core';
+import { describe, expect, it } from 'vitest';
+import { compileLayer } from '../compile';
+
+function area(names: string[]): ChartSpec {
+ return {
+ mark: 'area',
+ data: names.flatMap((series, index) =>
+ [1, 2, 3].map((x) => ({ x, series, value: 20 + index * 10 })),
+ ),
+ encoding: {
+ x: { field: 'x', type: 'quantitative' },
+ y: { field: 'value', type: 'quantitative' },
+ color: { field: 'series', type: 'nominal' },
+ },
+ endpointLabels: { placement: 'interior', anchor: 'band', ends: 'both' },
+ };
+}
+function input(legend: LegendConfig = {}, long = false): LayerSpec {
+ const names = long
+ ? [
+ 'Alpha comparison group',
+ 'Beta comparison group',
+ 'Gamma comparison group',
+ 'Delta comparison group',
+ ]
+ : ['A', 'B', 'C', 'D'];
+ return {
+ layer: [area(names.slice(0, 2)), area(names.slice(2))],
+ resolve: { scale: { y: 'independent' } },
+ legend,
+ chrome: {
+ title: 'Four independent series',
+ subtitle: 'The full legend has one measured reservation',
+ source: 'Source: fixture',
+ },
+ };
+}
+
+describe('independent layer categorical legend assembly', () => {
+ it.each([
+ 'top',
+ 'bottom',
+ 'right',
+ 'bottom-right',
+ ] as const)('places every merged %s entry within its reserved bounds', (position) => {
+ const layout = compileLayer(input({ position }), { width: 390, height: 500 });
+ const legend = layout.legend;
+ if (!('entries' in legend)) throw new Error('Expected categorical legend');
+ expect(legend.entries.map((entry) => entry.label)).toEqual(['A', 'B', 'C', 'D']);
+ expect(legend.entryPositions).toHaveLength(4);
+ for (const entry of legend.entryPositions!) {
+ expect(entry.x).toBeGreaterThanOrEqual(legend.bounds.x - 1e-6);
+ expect(entry.x + entry.width).toBeLessThanOrEqual(
+ legend.bounds.x + legend.bounds.width + 1e-6,
+ );
+ expect(entry.y).toBeGreaterThanOrEqual(legend.bounds.y - 1e-6);
+ expect(entry.y + legend.swatchSize).toBeLessThanOrEqual(
+ legend.bounds.y + legend.bounds.height + 1e-6,
+ );
+ }
+ if (position === 'top')
+ expect(legend.bounds.y + legend.bounds.height).toBeLessThanOrEqual(layout.area.y);
+ if (position === 'bottom')
+ expect(legend.bounds.y).toBeGreaterThanOrEqual(layout.area.y + layout.area.height);
+ if (position === 'right' || position === 'bottom-right')
+ expect(legend.bounds.x).toBeGreaterThanOrEqual(layout.area.x + layout.area.width);
+ });
+
+ it.each([
+ 390, 1280,
+ ])('reserves complete wrapped top legend rows without invading chrome at %spx', (width) => {
+ const layout = compileLayer(input({ position: 'top', maxRows: 4 }, true), {
+ width,
+ height: 600,
+ });
+ const legend = layout.legend;
+ if (!('entries' in legend)) throw new Error('Expected categorical legend');
+ expect(legend.entries).toHaveLength(4);
+ expect(legend.entryPositions).toHaveLength(4);
+ const last = Math.max(...legend.entryPositions!.map((entry) => entry.y + legend.swatchSize));
+ expect(last).toBeLessThanOrEqual(legend.bounds.y + legend.bounds.height + 1e-6);
+ expect(legend.bounds.y).toBeGreaterThanOrEqual(layout.chrome.topHeight);
+ expect(legend.bounds.y + legend.bounds.height).toBeLessThanOrEqual(layout.area.y);
+ });
+
+ it('keeps authored hiding and applies limits to the complete merged set', () => {
+ const hidden = compileLayer(input({ show: false }), { width: 390, height: 500 });
+ expect('entries' in hidden.legend ? hidden.legend.entries : []).toHaveLength(0);
+ const limited = compileLayer(input({ show: true, symbolLimit: 2 }), {
+ width: 390,
+ height: 500,
+ });
+ if (!('entries' in limited.legend)) throw new Error('Expected categorical legend');
+ expect(limited.legend.entries).toHaveLength(3);
+ expect(limited.legend.entries.slice(0, 2).map((entry) => entry.label)).toEqual(['A', 'B']);
+ expect(limited.legend.entries.at(-1)?.label).toContain('more');
+ });
+});
+
+it('collects transformed categories and does not invent an explicitly hidden leaf legend', () => {
+ const source: LayerSpec = {
+ ...input(),
+ legend: undefined,
+ layer: [
+ { ...area(['A', 'B']), legend: { show: false } },
+ {
+ ...area(['C', 'D']),
+ legend: { show: true, position: 'bottom' },
+ transform: [{ filter: { field: 'series', equal: 'D' } }],
+ },
+ ],
+ };
+ const layout = compileLayer(source, { width: 390, height: 500 });
+ if (!('entries' in layout.legend)) throw new Error('Expected categorical legend');
+ expect(layout.legend.entries.map((entry) => entry.label)).toEqual(['D']);
+ expect(layout.legend.position).toBe('bottom');
+ expect(layout.legend.entryPositions).toHaveLength(1);
+});
+
+it('shares the complete legend frame when no endpoint columns are enabled', () => {
+ const source = input({ position: 'top' });
+ source.layer = [area(['A', 'B']), area(['C', 'D'])].map((leaf) => ({
+ ...leaf,
+ endpointLabels: false,
+ }));
+ const layout = compileLayer(source, { width: 390, height: 500 });
+ if (!('entries' in layout.legend)) throw new Error('Expected categorical legend');
+ expect(layout.legend.entries).toHaveLength(4);
+ expect(layout.legend.entryPositions).toHaveLength(4);
+ for (const mark of layout.marks) {
+ if (mark.type !== 'area') continue;
+ for (const point of [...mark.topPoints, ...mark.bottomPoints]) {
+ expect(point.y).toBeGreaterThanOrEqual(layout.area.y - 1e-6);
+ expect(point.y).toBeLessThanOrEqual(layout.area.y + layout.area.height + 1e-6);
+ }
+ }
+});
+
+it.each([390, 1280])('reserves wrapped bottom rows before source chrome at %spx', (width) => {
+ const layout = compileLayer(input({ position: 'bottom', maxRows: 4 }, true), {
+ width,
+ height: 600,
+ });
+ const legend = layout.legend;
+ if (!('entries' in legend)) throw new Error('Expected categorical legend');
+ expect(legend.entryPositions).toHaveLength(4);
+ expect(legend.bounds.y).toBeGreaterThanOrEqual(layout.area.y + layout.area.height);
+ expect(legend.bounds.y + legend.bounds.height).toBeLessThanOrEqual(
+ (layout.chrome.bottomAnchorY ?? layout.area.y + layout.area.height) +
+ layout.chrome.source!.y +
+ (layout.chrome.noteBandHeight ?? 0),
+ );
+});
diff --git a/packages/engine/src/__tests__/layout-invariants.test.ts b/packages/engine/src/__tests__/layout-invariants.test.ts
index 6fa8ac55..3888586a 100644
--- a/packages/engine/src/__tests__/layout-invariants.test.ts
+++ b/packages/engine/src/__tests__/layout-invariants.test.ts
@@ -1,5 +1,4 @@
-import type { ChartLayout } from '@opendata-ai/openchart-core';
-import { afterAll, describe, expect, it } from 'vitest';
+import { describe, expect, it } from 'vitest';
import { compileChart } from '../compile';
import { checkLayoutInvariants } from './helpers/invariants';
@@ -62,20 +61,9 @@ const FULL_CHROME = {
const WIDTHS = [320, 800] as const;
const HEIGHT = 400;
-let compileSkipCount = 0;
-
-/**
- * Compile a spec and run layout invariants. Returns early (skip) if
- * compileChart throws due to pre-existing compile failures from plan merges.
- */
+/** Compile every fixture; a compile failure must fail the invariant test. */
function compileAndCheck(spec: Record, width: number, height = HEIGHT) {
- let layout: ChartLayout;
- try {
- layout = compileChart(spec, { width, height });
- } catch {
- compileSkipCount++;
- return;
- }
+ const layout = compileChart(spec, { width, height });
const violations = checkLayoutInvariants(layout, { svgWidth: width, svgHeight: height });
expect(violations, violations.join('\n')).toEqual([]);
}
@@ -85,12 +73,6 @@ function compileAndCheck(spec: Record, width: number, height =
// ---------------------------------------------------------------------------
describe('layout invariants', () => {
- afterAll(() => {
- if (compileSkipCount > 0) {
- console.warn(`layout invariants: ${compileSkipCount} tests skipped due to compile errors`);
- }
- });
-
// -----------------------------------------------------------------------
// Bar charts (horizontal: x=quantitative, y=nominal)
// -----------------------------------------------------------------------
@@ -292,7 +274,7 @@ describe('layout invariants', () => {
});
// -----------------------------------------------------------------------
- // Pie charts (arc mark: y=quantitative, color=nominal)
+ // Pie charts (arc mark: theta=quantitative, color=nominal)
// -----------------------------------------------------------------------
describe('pie charts', () => {
for (const width of WIDTHS) {
@@ -302,7 +284,7 @@ describe('layout invariants', () => {
mark: 'arc',
data: makePieData(SHORT_LABELS),
encoding: {
- y: { field: 'value', type: 'quantitative' },
+ theta: { field: 'value', type: 'quantitative' },
color: { field: 'category', type: 'nominal' },
},
chrome: TITLE_ONLY,
@@ -318,7 +300,7 @@ describe('layout invariants', () => {
mark: 'arc',
data: makePieData(SHORT_LABELS),
encoding: {
- y: { field: 'value', type: 'quantitative' },
+ theta: { field: 'value', type: 'quantitative' },
color: { field: 'category', type: 'nominal' },
},
legend: { position: 'bottom', show: true },
@@ -335,7 +317,7 @@ describe('layout invariants', () => {
mark: 'arc',
data: makePieData(LONG_LABELS),
encoding: {
- y: { field: 'value', type: 'quantitative' },
+ theta: { field: 'value', type: 'quantitative' },
color: { field: 'category', type: 'nominal' },
},
legend: { position: 'top', show: true },
@@ -351,7 +333,7 @@ describe('layout invariants', () => {
mark: 'arc',
data: makePieData(SHORT_LABELS),
encoding: {
- y: { field: 'value', type: 'quantitative' },
+ theta: { field: 'value', type: 'quantitative' },
color: { field: 'category', type: 'nominal' },
},
chrome: NO_CHROME,
@@ -437,7 +419,7 @@ describe('layout invariants', () => {
});
// -----------------------------------------------------------------------
- // Dot charts (circle mark: x=quantitative, y=quantitative)
+ // Dot charts (circle mark: x=quantitative, y=ordinal)
// -----------------------------------------------------------------------
describe('dot charts', () => {
for (const width of WIDTHS) {
@@ -448,7 +430,7 @@ describe('layout invariants', () => {
data: makeScatterData(['Alpha', 'Beta']),
encoding: {
x: { field: 'x', type: 'quantitative' },
- y: { field: 'y', type: 'quantitative' },
+ y: { field: 'y', type: 'ordinal' },
color: { field: 'group', type: 'nominal' },
},
legend: { position: 'bottom', show: true },
@@ -466,7 +448,7 @@ describe('layout invariants', () => {
data: makeScatterData(['Very Long Group Name A', 'Very Long Group Name B']),
encoding: {
x: { field: 'x', type: 'quantitative' },
- y: { field: 'y', type: 'quantitative' },
+ y: { field: 'y', type: 'ordinal' },
color: { field: 'group', type: 'nominal' },
},
legend: { position: 'top', show: true },
diff --git a/packages/engine/src/__tests__/mark-keys.test.ts b/packages/engine/src/__tests__/mark-keys.test.ts
index 531efe2e..f5e38323 100644
--- a/packages/engine/src/__tests__/mark-keys.test.ts
+++ b/packages/engine/src/__tests__/mark-keys.test.ts
@@ -34,12 +34,9 @@ describe('column chart mark keys', () => {
const rects = layout.marks.filter((m) => m.type === 'rect');
expect(rects.length).toBe(3);
- for (const m of rects) {
- expect((m as { key?: string }).key).toBeDefined();
- }
-
+ // Keyed by series|category; a single-series column has an empty series part.
const keys = rects.map((m) => (m as { key?: string }).key);
- expect(new Set(keys).size).toBe(3);
+ expect(keys).toEqual(['|A', '|B', '|C']);
});
it('deduplicates keys for duplicate category values', () => {
@@ -58,8 +55,9 @@ describe('column chart mark keys', () => {
const layout = compileChart(spec, dims);
const rects = layout.marks.filter((m) => m.type === 'rect');
const keys = rects.map((m) => (m as { key?: string }).key);
- // All keys must be unique even with duplicate categories
- expect(new Set(keys).size).toBe(keys.length);
+ // All keys must be unique even with duplicate categories: repeats get an
+ // occurrence suffix, unique keys pass through unchanged.
+ expect(keys).toEqual(['|A⸱0', '|A⸱1', '|B']);
});
it('stamps keys on grouped columns', () => {
@@ -82,10 +80,7 @@ describe('column chart mark keys', () => {
expect(rects.length).toBeGreaterThanOrEqual(4);
const keys = rects.map((m) => (m as { key?: string }).key);
- for (const k of keys) {
- expect(k).toBeDefined();
- }
- expect(new Set(keys).size).toBe(keys.length);
+ expect(keys).toEqual(['Revenue|Q1', 'Cost|Q1', 'Revenue|Q2', 'Cost|Q2']);
});
});
@@ -110,10 +105,7 @@ describe('bar chart mark keys', () => {
const layout = compileChart(spec, dims);
const rects = layout.marks.filter((m) => m.type === 'rect');
const keys = rects.map((m) => (m as { key?: string }).key);
- for (const k of keys) {
- expect(k).toBeDefined();
- }
- expect(new Set(keys).size).toBe(keys.length);
+ expect(keys).toEqual(['|Alpha', '|Beta', '|Gamma']);
});
});
@@ -141,11 +133,9 @@ describe('line chart mark keys', () => {
const lines = layout.marks.filter((m) => m.type === 'line');
expect(lines.length).toBe(2);
+ // One line per series, keyed by the series value.
const keys = lines.map((m) => (m as { key?: string }).key);
- for (const k of keys) {
- expect(k).toBeDefined();
- }
- expect(new Set(keys).size).toBe(2);
+ expect(keys).toEqual(['US', 'UK']);
});
it('stamps pointKeys on line marks with temporal x', () => {
@@ -166,15 +156,8 @@ describe('line chart mark keys', () => {
expect(lines.length).toBe(1);
const line = lines[0] as { pointKeys?: string[] };
- expect(line.pointKeys).toBeDefined();
- expect(line.pointKeys!.length).toBe(3);
-
- // Each point key should be a unique, defined string
- for (const pk of line.pointKeys!) {
- expect(pk).toBeDefined();
- expect(typeof pk).toBe('string');
- }
- expect(new Set(line.pointKeys!).size).toBe(3);
+ // Point keys serialize each datum's x value.
+ expect(line.pointKeys).toEqual(['2020-01-01', '2021-01-01', '2022-01-01']);
});
});
@@ -200,11 +183,9 @@ describe('scatter chart mark keys', () => {
const points = layout.marks.filter((m) => m.type === 'point');
expect(points.length).toBe(3);
+ // No key field or color: keys derive from the x value (spec-reference, encoding.key).
const keys = points.map((m) => (m as { key?: string }).key);
- for (const k of keys) {
- expect(k).toBeDefined();
- }
- expect(new Set(keys).size).toBe(3);
+ expect(keys).toEqual(['10', '30', '50']);
});
it('uses encoding.key field when specified', () => {
@@ -245,7 +226,7 @@ describe('pie chart mark keys', () => {
{ category: 'Tablet', share: 10 },
],
encoding: {
- y: { field: 'share', type: 'quantitative' },
+ theta: { field: 'share', type: 'quantitative' },
color: { field: 'category', type: 'nominal' },
},
};
@@ -254,10 +235,7 @@ describe('pie chart mark keys', () => {
expect(arcs.length).toBe(3);
const keys = arcs.map((m) => (m as { key?: string }).key);
- for (const k of keys) {
- expect(k).toBeDefined();
- }
- expect(new Set(keys).size).toBe(3);
+ expect(keys).toEqual(['Desktop', 'Mobile', 'Tablet']);
});
});
@@ -286,9 +264,6 @@ describe('area chart mark keys', () => {
expect(areas.length).toBe(2);
const keys = areas.map((m) => (m as { key?: string }).key);
- for (const k of keys) {
- expect(k).toBeDefined();
- }
- expect(new Set(keys).size).toBe(2);
+ expect(keys).toEqual(['A', 'B']);
});
});
diff --git a/packages/engine/src/__tests__/migrate-spec-v8.test.ts b/packages/engine/src/__tests__/migrate-spec-v8.test.ts
index 59ec70f2..22485b33 100644
--- a/packages/engine/src/__tests__/migrate-spec-v8.test.ts
+++ b/packages/engine/src/__tests__/migrate-spec-v8.test.ts
@@ -1,5 +1,8 @@
+import { broadsheet, essay, resolveTheme, terminal, wire } from '@opendata-ai/openchart-core';
import { describe, expect, it } from 'vitest';
+import v8Fixtures from '../__test-fixtures__/v8-styling.json';
import { compileChart, compileLayer } from '../compile';
+import { validateSpec } from '../compiler/validate';
import { migrateSpecV8 } from '../migration/v8';
const chart = {
@@ -11,6 +14,35 @@ const chart = {
encoding: { x: { field: 'x', type: 'quantitative' }, y: { field: 'y', type: 'quantitative' } },
};
describe('migrateSpecV8 alias repairs', () => {
+ it('reports measured attribution as a layout limitation without repeated repairs', () => {
+ const result = migrateSpecV8({ ...chart, chrome: { source: 'Source: Research team' } });
+ expect(result.repairs.some((repair) => /measured attribution/.test(repair.reason))).toBe(true);
+ expect(migrateSpecV8(result.spec).repairs).toEqual([]);
+ });
+ it('preserves hardcoded v8 reference typography independently of the small theme size', () => {
+ const source = {
+ ...chart,
+ theme: { fonts: { sizes: { small: 20 } } },
+ annotations: [
+ { type: 'refline', y: 0, label: 'Threshold' },
+ { type: 'range', x1: 0, x2: 1, label: 'Interval' },
+ { type: 'refline', y: 1, label: 'Authored', fontSize: 17 },
+ ],
+ };
+ const migrated = migrateSpecV8(source);
+ expect(
+ compileChart(migrated.spec, { width: 800, height: 600 }).annotations.map(
+ (a) => a.label?.style.fontSize,
+ ),
+ ).toEqual([11, 11, 17]);
+ expect(migrateSpecV8(migrated.spec).repairs).toEqual([]);
+ expect(
+ compileChart(migrateSpecV8(source, { styling: 'v9' }).spec, {
+ width: 800,
+ height: 600,
+ }).annotations.map((a) => a.label?.style.fontSize),
+ ).toEqual([20, 20, 17]);
+ });
it.each([
[
{ type: 'table', compact: true },
@@ -24,6 +56,11 @@ describe('migrateSpecV8 alias repairs', () => {
{ type: 'map', valueFormat: '.2f', encoding: { color: { field: 'v' } } },
{ type: 'map', encoding: { color: { field: 'v', format: '.2f' } } },
],
+ [{ type: 'map', valueFormat: '.2f' }, { type: 'map' }],
+ [
+ { type: 'barlist', valueFormat: '.2f', encoding: { label: { field: 'k' } } },
+ { type: 'barlist', encoding: { label: { field: 'k' } } },
+ ],
[
{ type: 'sankey', valueFormat: '.2f', encoding: { value: { field: 'v', format: '.1f' } } },
{ type: 'sankey', encoding: { value: { field: 'v', format: '.1f' } } },
@@ -51,6 +88,24 @@ describe('migrateSpecV8 alias repairs', () => {
expect(migrateSpecV8(result.spec)).toEqual({ spec: result.spec, repairs: [] });
});
+ it('drops a channel-less map valueFormat so the migrated spec validates', () => {
+ const { spec, repairs } = migrateSpecV8({
+ type: 'map',
+ geography: 'us-states',
+ data: [],
+ valueFormat: '.1f',
+ points: {
+ data: [{ lat: 1, lon: 2 }],
+ encoding: { latitude: { field: 'lat' }, longitude: { field: 'lon' } },
+ },
+ });
+ expect(spec).not.toHaveProperty('valueFormat');
+ expect(repairs.find((r) => r.path === '/valueFormat')?.reason).toMatch(/no effect/);
+ expect(validateSpec(spec).errors.filter((e) => /valueFormat/.test(e.path + e.message))).toEqual(
+ [],
+ );
+ });
+
it('keeps record tilemap valueFormat', () => {
const spec = { type: 'tilemap', data: { CA: 4 }, valueFormat: '.2f' };
expect(migrateSpecV8(spec).spec).toMatchObject(spec);
@@ -63,7 +118,7 @@ describe('migrateSpecV8 alias repairs', () => {
expect(result.repairs.map((r) => r.path)).toContain('/layer/0/annotations/0/type');
});
- it('preserves rendering for nonvisual aliases', () => {
+ it('compiles the repaired aliases under v9', () => {
const spec = {
...chart,
animation: { preset: 'legacy' },
@@ -71,9 +126,11 @@ describe('migrateSpecV8 alias repairs', () => {
theme: { chrome: { title: '#333333' } },
chrome: { title: 'Title' },
};
- expect(compileChart(migrateSpecV8(spec).spec, { width: 640, height: 400 })).toEqual(
- compileChart(spec, { width: 640, height: 400 }),
- );
+ const layout = compileChart(migrateSpecV8(spec).spec, { width: 640, height: 400 });
+ expect(layout.annotations[0].type).toBe('refline');
+ expect(layout.theme.chrome.title.color).toBe('#333333');
+ // The legacy preset is dropped, so the default entrance applies.
+ expect(layout.animation?.enter?.duration).toBe(450);
});
it('replaces legacy labels and records each lost offset (explicit 8.10 exception)', () => {
@@ -152,21 +209,15 @@ it('rewrites the parent endpoint owner of a legacy-label layer', () => {
const migrated = migrateSpecV8(spec);
expect(migrated.spec.endpointLabels).toEqual({ content: 'label', showMarker: false });
expect(migrated.repairs.map((repair) => repair.path)).toContain('/endpointLabels');
- expect(compileLayer(migrated.spec, { width: 1280, height: 400 }).endpointLabels).toBeDefined();
+ const endpointLabels = compileLayer(migrated.spec, { width: 1280, height: 400 }).endpointLabels;
+ expect(endpointLabels!.entries.map((e) => e.seriesKey).sort()).toEqual(['A', 'B']);
expect(migrateSpecV8(migrated.spec).repairs).toEqual([]);
});
-it('warns once for legacy chrome colors supplied through compile options', () => {
- const warnings: string[] = [];
- const options = {
- width: 640,
- height: 400,
- theme: { chrome: { title: 'red' } },
- onWarn: (message: string) => warnings.push(message),
- };
- compileChart(chart, options);
- compileChart(chart, options);
- expect(warnings.filter((message) => message.includes('theme.chrome'))).toHaveLength(1);
+it('rejects removed chrome color strings in compile options', () => {
+ expect(() =>
+ compileChart(chart, { width: 640, height: 400, theme: { chrome: { title: 'red' } } } as never),
+ ).toThrow('theme.chrome');
});
describe('migrateSpecV8 default materialization', () => {
@@ -221,48 +272,408 @@ describe('migrateSpecV8 default materialization', () => {
);
if (responsive === false || responsive === undefined) expect(migrated.responsive).toBe(false);
});
- it.each([
- { chrome: { title: { fontSize: 31 }, source: { fontSize: 16 } } },
- { fonts: { sizes: { small: 15, title: 32 } } },
- { fonts: { sizes: { body: 16 } }, chrome: { footer: { fontSize: 19 } } },
- ])('preserves authored typography %j', (theme) => {
- const source = {
- ...chart,
- theme,
- chrome: { title: 'Title', source: 'Source', footer: 'Footer' },
- };
- const migrated = migrateSpecV8(source).spec;
- expect(compileChart(migrated, { width: 640, height: 500 }).chrome).toEqual(
- compileChart(source, { width: 640, height: 500 }).chrome,
- );
+});
+
+it('can adopt v9 styling while performing every necessary alias repair', () => {
+ const source = {
+ ...chart,
+ darkMode: 'off',
+ annotations: [
+ { type: 'rule', y: 0, label: 'Rule' },
+ { type: 'text', x: 0, y: 0, text: 'Text', subtitle: 'Details' },
+ ],
+ };
+ const migrated = migrateSpecV8(source, { styling: 'v9' });
+ expect(migrated.spec.annotations[0].type).toBe('refline');
+ expect(migrated.spec).not.toHaveProperty('darkMode');
+ expect(migrated.spec).not.toHaveProperty('theme');
+ expect(migrated.spec.annotations[1]).not.toHaveProperty('fontWeight');
+ expect(migrateSpecV8(migrated.spec, { styling: 'v9' }).repairs).toEqual([]);
+});
+
+// Captured from released v8.9.1 (8baef9e8), not regenerated by v9 snapshots.
+// Layout coordinates intentionally are not part of the styling compatibility promise.
+it.each(v8Fixtures)('preserves released v8 styling: $name', ({ source, expected }) => {
+ const before = structuredClone(source);
+ const { spec } = migrateSpecV8(source);
+ const compile = 'layer' in source ? compileLayer : compileChart;
+ const layout = compile(spec as never, { width: 640, height: 500 });
+ expect(source).toEqual(before);
+ expect(migrateSpecV8(spec)).toEqual({ spec, repairs: [] });
+ // New resolved control metadata did not exist in the released capture.
+ // Compare every old field unchanged; assert each added endpoint constant separately.
+ const { endpointLabel, endpointValue, ...releasedSizes } = layout.theme.fonts.sizes;
+ const { endpointEntryGap, endpointValueGap, endpointColumnGap, ...releasedSpacing } =
+ layout.theme.spacing;
+ expect({ ...layout.theme.fonts, sizes: releasedSizes }).toEqual(expected.fonts);
+ expect(releasedSpacing).toEqual(expected.spacing);
+ expect({ endpointLabel, endpointValue }).toEqual({ endpointLabel: 13, endpointValue: 12 });
+ expect({ endpointEntryGap, endpointValueGap, endpointColumnGap }).toEqual({
+ endpointEntryGap: 6,
+ endpointValueGap: 4,
+ endpointColumnGap: 18,
});
+ expect(layout.theme.chrome).toEqual(expected.chrome);
+ const { gridlineZero, mapInteriorBorder, mapOutlineBorder, ...releasedColors } =
+ layout.theme.colors;
+ expect(releasedColors).toEqual(expected.colors);
+ expect(gridlineZero).toBe(layout.theme.colors.gridline);
+ // Map roles were introduced after the released capture.
+ expect(mapInteriorBorder).toBe(
+ layout.theme.isDark ? 'rgba(255, 255, 255, 0.18)' : 'rgba(0,0,0,0.14)',
+ );
+ expect(mapOutlineBorder).toBe(
+ layout.theme.isDark ? 'rgba(255, 255, 255, 0.38)' : 'rgba(0,0,0,0.40)',
+ );
+ const annotations = layout.annotations.map((a) => ({
+ type: a.type,
+ label: a.label ? { text: a.label.text, style: a.label.style } : undefined,
+ subtitle: a.subtitle ? { text: a.subtitle.text, style: a.subtitle.style } : undefined,
+ connector: a.label?.connector
+ ? { arrow: a.label.connector.arrow, stroke: a.label.connector.stroke }
+ : undefined,
+ }));
+ expect(JSON.parse(JSON.stringify(annotations))).toEqual(expected.annotations);
+ if ('marks' in expected)
+ expect(
+ JSON.parse(
+ JSON.stringify(
+ layout.marks.map((m) => ({
+ type: m.type,
+ fill: 'fill' in m ? m.fill : undefined,
+ stroke: 'stroke' in m ? m.stroke : undefined,
+ })),
+ ),
+ ),
+ ).toEqual(expected.marks);
+});
+
+it('keeps v8 general font sizes ahead of authored chrome sizes', () => {
+ const source = {
+ ...chart,
+ chrome: { title: 'T', subtitle: 'S', source: 'S', byline: 'B', footer: 'F' },
+ theme: {
+ fonts: { sizes: { title: 30, subtitle: 17, small: 13 } },
+ chrome: {
+ title: { fontSize: 18 },
+ subtitle: { fontSize: 9 },
+ source: { fontSize: 10 },
+ byline: { fontSize: 8 },
+ footer: { fontSize: 7, color: 'red' },
+ },
+ },
+ };
+ const { spec, repairs } = migrateSpecV8(source);
+ const { chrome } = compileChart(spec, { width: 640, height: 400 }).theme;
+ // Captured from released v8.9.1.
+ expect(
+ ['title', 'subtitle', 'source', 'byline', 'footer'].map(
+ (key) => chrome[key as keyof typeof chrome].fontSize,
+ ),
+ ).toEqual([30, 17, 13, 13, 13]);
+ expect(chrome.footer.color).toBe('red');
+ expect(repairs.map((r) => r.path)).toContain('/theme/chrome/title/fontSize');
+ expect(migrateSpecV8(spec).repairs).toEqual([]);
+ const v9 = migrateSpecV8(source, { styling: 'v9' }).spec;
+ expect(compileChart(v9, { width: 640, height: 400 }).theme.chrome.title.fontSize).toBe(18);
+});
+
+it('keeps each leaf palette while the layer theme keeps owning root chrome', () => {
+ const source = {
+ theme: { colors: { categorical: ['#111111'] }, fonts: { sizes: { title: 30 } } },
+ chrome: { title: 'Title' },
+ layer: [
+ {
+ ...chart,
+ theme: { colors: { categorical: ['#00ff00'] }, fonts: { sizes: { title: 12 } } },
+ },
+ { ...chart, theme: { colors: { categorical: ['#ff0000'] } } },
+ ],
+ };
+ const { spec } = migrateSpecV8(source);
+ const layout = compileLayer(spec as never, { width: 640, height: 500 });
+ // Captured from released v8.9.1.
+ expect(layout.marks.filter((m) => m.type === 'point').map((m) => m.fill)).toEqual([
+ '#00ff00',
+ '#00ff00',
+ '#ff0000',
+ '#ff0000',
+ ]);
+ expect(layout.theme.chrome.title.fontSize).toBe(30);
+ expect(migrateSpecV8(spec).repairs).toEqual([]);
+ // v9 resolves root and leaf themes the way v8 did, so the leaf keeps its
+ // own non-color keys rather than taking a copy of the parent theme.
+ expect(spec.layer[0].theme.fonts.sizes.title).toBe(12);
+ expect(spec.layer[0].theme.colors.categorical).toEqual(['#00ff00']);
});
-it('does not merge a parent theme into an authored partial child theme', () => {
+it('repairs aliases in a line-layer before inspecting its legacy label policy', () => {
const source = {
- theme: { colors: { categorical: ['green'] } },
- layer: [chart, { ...chart, theme: { fonts: { sizes: { small: 16 } } } }],
+ layer: [
+ {
+ ...chart,
+ mark: 'line',
+ annotations: [{ type: 'rule', y: 0, label: 'Baseline' }],
+ encoding: {
+ ...chart.encoding,
+ x: { field: 'x', type: 'ordinal' },
+ radius: { field: 'unused' },
+ },
+ },
+ ],
};
- const opts = { width: 640, height: 500 };
- expect(compileLayer(migrateSpecV8(source).spec, opts).marks).toEqual(
- compileLayer(source, opts).marks,
+ const result = migrateSpecV8(source);
+ expect(result.spec.layer[0].annotations[0].type).toBe('refline');
+ expect(result.spec.layer[0].encoding).not.toHaveProperty('radius');
+ const layout = compileLayer(result.spec, { width: 640, height: 400 });
+ expect(layout.annotations.map((a) => a.type)).toEqual(['refline']);
+ expect(migrateSpecV8(result.spec).repairs).toEqual([]);
+});
+
+it('drops dual-axis leaf themes that v8 ignored under a layer theme', () => {
+ const leafWith = (fill: string) => ({ ...chart, theme: { colors: { categorical: [fill] } } });
+ const source = {
+ theme: { colors: { categorical: ['#111111'] } },
+ resolve: { scale: { y: 'independent' } },
+ layer: [leafWith('#00ff00'), leafWith('#ff0000')],
+ };
+ const { spec, repairs } = migrateSpecV8(source);
+ expect(spec.layer.map((leaf: { theme?: unknown }) => leaf.theme)).toEqual([undefined, undefined]);
+ expect(repairs.map((r) => r.path)).toEqual(
+ expect.arrayContaining(['/layer/0/theme', '/layer/1/theme']),
);
+ expect(migrateSpecV8(spec).repairs).toEqual([]);
+ const layout = compileLayer(spec as never, { width: 640, height: 500 });
+ // v8.9.1 drew both sides with the layer palette.
+ expect(
+ new Set(
+ layout.marks.filter((m) => m.type === 'point').map((m) => (m as { fill: string }).fill),
+ ),
+ ).toEqual(new Set(['#111111']));
+ // Without a layer theme each leaf kept its own in v8, so nothing changes.
+ const { theme: _omit, ...noLayerTheme } = source;
+ // (The leaf theme itself stays, keeping its own palette.)
+ expect(migrateSpecV8(noLayerTheme).spec.layer[0].theme.colors.categorical).toEqual(['#00ff00']);
});
-it.each([false, true])('preserves first-leaf ownership in nested=%s layers', (nested) => {
- const leaf = {
+it('materializes released v8 endpoint constants without replacing authored controls', () => {
+ const defaults = migrateSpecV8(chart).spec as typeof chart & {
+ theme: { fonts: { sizes: Record }; spacing: Record };
+ };
+ expect(defaults.theme.fonts.sizes).toMatchObject({ endpointLabel: 13, endpointValue: 12 });
+ expect(defaults.theme.spacing).toMatchObject({
+ endpointEntryGap: 6,
+ endpointValueGap: 4,
+ endpointColumnGap: 18,
+ });
+ const source = {
...chart,
- responsive: false,
- theme: { colors: { background: 'pink' }, fonts: { sizes: { title: 40 } } },
- chrome: { title: 'Owned by first leaf' },
- annotations: [{ type: 'text', x: 0, y: 0, text: 'Hidden' }],
+ theme: { fonts: { sizes: { endpointLabel: 17 } }, spacing: { endpointColumnGap: 24 } },
+ };
+ const migrated = migrateSpecV8(source);
+ expect(migrated.spec.theme.fonts.sizes.endpointLabel).toBe(17);
+ expect(migrated.spec.theme.spacing.endpointColumnGap).toBe(24);
+ expect(migrateSpecV8(migrated.spec).repairs).toEqual([]);
+ expect(migrateSpecV8(chart, { styling: 'v9' }).spec).not.toHaveProperty('theme');
+});
+
+it('reports the value-label font correction once without offering a false v8 styling guarantee', () => {
+ const source = {
+ mark: 'bar',
+ data: [{ name: 'A', value: 4 }],
+ encoding: {
+ x: { field: 'value', type: 'quantitative' },
+ y: { field: 'name', type: 'nominal' },
+ },
+ theme: { fonts: { family: 'Georgia, serif' } },
};
- const source = { layer: [nested ? { layer: [leaf] } : leaf, chart] };
- const opts = { width: 390, height: 500 };
- const before = compileLayer(source, opts),
- after = compileLayer(migrateSpecV8(source).spec, opts);
- expect(after.annotations).toEqual(before.annotations);
- expect(after.chrome).toEqual(before.chrome);
- expect(after.theme.colors).toEqual(before.theme.colors);
- expect(after.marks).toEqual(before.marks);
+ const migrated = migrateSpecV8(source);
+ expect(
+ migrated.repairs.some((repair) =>
+ repair.reason.includes('value labels now follow the resolved theme font'),
+ ),
+ ).toBe(true);
+ const layout = compileChart(migrated.spec, { width: 680, height: 400 });
+ expect(
+ layout.marks.some(
+ (mark) => mark.type === 'rect' && mark.label?.style.fontFamily === 'Georgia, serif',
+ ),
+ ).toBe(true);
+ expect(migrateSpecV8(migrated.spec).repairs).toEqual([]);
+});
+
+describe('approved B3 defaults preserve v8 styling on migration', () => {
+ it('pins old scaffolding ink in both modes and leaves v9 adoption unmaterialized', () => {
+ const migrated = migrateSpecV8(chart);
+ expect(migrated.spec).toMatchObject({
+ theme: {
+ colors: {
+ gridline: { light: 'rgba(0,0,0,0.08)', dark: 'rgba(255,255,255,0.06)' },
+ gridlineZero: { light: 'rgba(0,0,0,0.08)', dark: 'rgba(255,255,255,0.06)' },
+ },
+ },
+ });
+ expect(migrateSpecV8(migrated.spec).repairs).toEqual([]);
+ expect(migrateSpecV8(chart, { styling: 'v9' }).spec).not.toHaveProperty('theme');
+ for (const darkMode of [false, true]) {
+ const old = compileChart(migrated.spec, { width: 640, height: 400, darkMode }).theme;
+ expect(old.colors.gridline).toBe(darkMode ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.08)');
+ expect(old.colors.gridlineZero).toBe(old.colors.gridline);
+ const adopted = compileChart(migrateSpecV8(chart, { styling: 'v9' }).spec, {
+ width: 640,
+ height: 400,
+ darkMode,
+ }).theme;
+ expect(adopted.colors.gridline).toBe(
+ darkMode ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.05)',
+ );
+ expect(adopted.colors.gridlineZero).toBe(
+ darkMode ? 'rgba(255,255,255,0.24)' : 'rgba(0,0,0,0.22)',
+ );
+ }
+ });
+ it('preserves authored gridline/zero and map-border modes', () => {
+ const gridline = { light: '#abcdef', dark: '#fedcba' };
+ const source = { ...chart, theme: { colors: { gridline } } };
+ expect(migrateSpecV8(source).spec).toMatchObject({
+ theme: { colors: { gridline, gridlineZero: gridline } },
+ });
+ const colors = {
+ gridline,
+ gridlineZero: '#445566',
+ mapInteriorBorder: '#123456',
+ mapOutlineBorder: { light: '#654321', dark: '#abcdef' },
+ };
+ expect(migrateSpecV8({ type: 'map', theme: { colors } }).spec).toMatchObject({
+ theme: { colors },
+ });
+ });
+ it('pins the old map borders and reports unpreservable map-key layout', () => {
+ const result = migrateSpecV8({ type: 'map' });
+ expect(result.spec).toMatchObject({
+ theme: {
+ colors: {
+ mapInteriorBorder: { light: 'rgba(0,0,0,0.25)', dark: 'rgba(255,255,255,0.22)' },
+ mapOutlineBorder: { light: 'rgba(0,0,0,0.55)', dark: 'rgba(255,255,255,0.45)' },
+ },
+ },
+ });
+ expect(
+ result.repairs.some((r) => /static map captions.*size-key placement/.test(r.reason)),
+ ).toBe(true);
+ expect(migrateSpecV8(result.spec).repairs).toEqual([]);
+ });
+ it('pins boundary anchors without overriding authored anchor, disabled labels or unstacked areas', () => {
+ const area = { ...chart, mark: 'area', endpointLabels: { ends: 'both' } };
+ expect(migrateSpecV8(area).spec.endpointLabels).toEqual({ ends: 'both', anchor: 'boundary' });
+ expect(migrateSpecV8({ ...area, endpointLabels: false }).spec.endpointLabels).toBe(false);
+ expect(
+ migrateSpecV8({ ...area, endpointLabels: { anchor: 'band' } }).spec.endpointLabels,
+ ).toEqual({ anchor: 'band' });
+ expect(
+ migrateSpecV8({
+ ...area,
+ encoding: { ...area.encoding, y: { ...area.encoding.y, stack: null } },
+ }).spec.endpointLabels,
+ ).toEqual({ ends: 'both' });
+ expect(migrateSpecV8(area, { styling: 'v9' }).spec.endpointLabels).toEqual({ ends: 'both' });
+ const result = migrateSpecV8(area);
+ expect(migrateSpecV8(result.spec).repairs).toEqual([]);
+ });
+ it('keeps theme-less layer inheritance and an inherited authored anchor', () => {
+ const source = {
+ theme: { colors: { gridline: '#abcdef' } },
+ endpointLabels: { anchor: 'band' },
+ layer: [{ ...chart, mark: 'area' }],
+ };
+ const result = migrateSpecV8(source);
+ expect(result.spec.layer[0]).not.toHaveProperty('theme');
+ expect(result.spec.layer[0]).not.toHaveProperty('endpointLabels');
+ expect(result.spec.theme).toMatchObject({
+ colors: { gridline: '#abcdef', gridlineZero: '#abcdef' },
+ });
+ expect(migrateSpecV8(result.spec).repairs).toEqual([]);
+ });
+});
+
+it.each([
+ broadsheet,
+ essay,
+ terminal,
+ wire,
+])('preserves preset gridline modes and keeps zero on the same ink', (theme) => {
+ const result = migrateSpecV8({ ...chart, theme });
+ for (const darkMode of [false, true]) {
+ const layout = compileChart(result.spec, { width: 640, height: 400, darkMode });
+ const authored =
+ theme.colors && !Array.isArray(theme.colors) ? theme.colors.gridline : undefined;
+ expect(layout.theme.colors.gridline).toBe(
+ typeof authored === 'string' ? authored : authored?.[darkMode ? 'dark' : 'light'],
+ );
+ expect(layout.theme.colors.gridlineZero).toBe(layout.theme.colors.gridline);
+ }
+ expect(migrateSpecV8(result.spec).repairs).toEqual([]);
+});
+
+it('preserves inherited area endpoint visibility and boundary geometry', () => {
+ const area = {
+ mark: 'area',
+ data: [
+ { x: 0, y: 2, s: 'A' },
+ { x: 1, y: 3, s: 'A' },
+ { x: 0, y: 4, s: 'B' },
+ { x: 1, y: 5, s: 'B' },
+ ],
+ encoding: { ...chart.encoding, color: { field: 's', type: 'nominal' } },
+ };
+ for (const endpointLabels of [undefined, false, { show: false }]) {
+ const result = migrateSpecV8({ ...area, endpointLabels });
+ expect(result.spec.endpointLabels).toEqual(endpointLabels);
+ expect(compileChart(result.spec, { width: 800, height: 500 }).endpointLabels).toBeUndefined();
+ }
+ for (const independent of [false, true]) {
+ const source = {
+ endpointLabels: { ends: 'both' },
+ layer: [area],
+ ...(independent ? { resolve: { scale: { y: 'independent' } } } : {}),
+ };
+ const result = migrateSpecV8(source);
+ expect(result.spec.endpointLabels).toEqual({ ends: 'both', anchor: 'boundary' });
+ const layout = compileLayer(result.spec as never, { width: 800, height: 500 });
+ expect(layout.endpointLabels?.entries).toHaveLength(2);
+ for (const entry of layout.endpointLabels!.entries) {
+ const mark = layout.marks.find(
+ (mark) => mark.type === 'area' && mark.seriesKey === entry.seriesKey,
+ );
+ if (mark?.type !== 'area') throw new Error('Missing area for endpoint');
+ expect(entry.dataY).toBe(mark.dataPoints!.at(-1)!.y);
+ expect(entry.source?.datum.y).toBe(entry.seriesKey === 'A' ? 3 : 5);
+ }
+ expect(migrateSpecV8(result.spec).repairs).toEqual([]);
+ }
+});
+
+it('preserves old map border ink on an authored dark canvas without a dark-mode option', () => {
+ const result = migrateSpecV8({ type: 'map', theme: { colors: { background: '#111111' } } });
+ const theme = resolveTheme(result.spec.theme);
+ expect(theme.colors.mapInteriorBorder).toBe('rgba(255,255,255,0.22)');
+ expect(theme.colors.mapOutlineBorder).toBe('rgba(255,255,255,0.45)');
+ expect(theme.colors.gridlineZero).toBe(theme.colors.gridline);
+ expect(migrateSpecV8(result.spec).repairs).toEqual([]);
+});
+
+it('pins an area endpoint override enabled at a breakpoint without enabling the base chart', () => {
+ const source = {
+ ...chart,
+ mark: 'area',
+ endpointLabels: false,
+ overrides: { compact: { endpointLabels: { ends: 'both' } } },
+ };
+ const result = migrateSpecV8(source);
+ expect(result.spec.endpointLabels).toBe(false);
+ expect(result.spec.overrides.compact.endpointLabels).toEqual({
+ ends: 'both',
+ anchor: 'boundary',
+ });
+ expect(migrateSpecV8(result.spec).repairs).toEqual([]);
});
diff --git a/packages/engine/src/__tests__/scales.test.ts b/packages/engine/src/__tests__/scales.test.ts
index dbbb25d7..6be482de 100644
--- a/packages/engine/src/__tests__/scales.test.ts
+++ b/packages/engine/src/__tests__/scales.test.ts
@@ -46,10 +46,10 @@ const barSpec: NormalizedChartSpec = {
const chartArea = { x: 50, y: 50, width: 500, height: 300 };
describe('computeScales', () => {
- it('creates a time scale for temporal x encoding', () => {
+ it('creates a UTC time scale for date-only temporal x encoding', () => {
const scales = computeScales(lineSpec, chartArea, lineSpec.data);
expect(scales.x).toBeDefined();
- expect(scales.x!.type).toBe('time');
+ expect(scales.x!.type).toBe('utc');
// The scale should map dates within the range
const pos = scales.x!.scale(new Date('2020-06-01'));
@@ -312,7 +312,7 @@ describe('scale config properties', () => {
it('backward compatible: existing specs still work', () => {
// lineSpec and barSpec from before should still produce valid scales
const lineScales = computeScales(lineSpec, chartArea, lineSpec.data);
- expect(lineScales.x!.type).toBe('time');
+ expect(lineScales.x!.type).toBe('utc');
expect(lineScales.y!.type).toBe('linear');
expect(lineScales.color!.type).toBe('ordinal');
diff --git a/packages/engine/src/__tests__/series-color-parity.test.ts b/packages/engine/src/__tests__/series-color-parity.test.ts
index a9d02f8b..7d26937a 100644
--- a/packages/engine/src/__tests__/series-color-parity.test.ts
+++ b/packages/engine/src/__tests__/series-color-parity.test.ts
@@ -3,8 +3,7 @@
*
* A line's stroke is darkened on a light canvas (`adaptSeriesStroke`) because
* the palette hues are tuned as fills. Every place that echoes a series color
- * as chrome — the legend swatch, the endpoint-label column, the end-of-line
- * direct label, the area's top edge — has to show the SAME rendered color, or
+ * as chrome — the legend swatch, the endpoint-label column, the area's top edge — has to show the SAME rendered color, or
* the reader sees one cyan in the legend and a different one on the line.
*/
@@ -139,7 +138,7 @@ describe('series color parity', () => {
});
it('legend swatches match line strokes in dark mode too', () => {
- const layout = compileChart(lineSpec({ darkMode: 'force' }), SIZE);
+ const layout = compileChart(lineSpec(), { ...SIZE, darkMode: true });
const strokes = lineStrokes(layout);
for (const entry of layout.legend.entries) {
expect(entry.color).toBe(strokes.get(entry.label));
@@ -194,22 +193,73 @@ describe('series color parity', () => {
}
});
- it('end-of-line direct labels carry the rendered stroke color', () => {
+ it.each([
+ false,
+ { show: false },
+ ] as const)('does not recreate legacy line labels when both surfaces are hidden (%j)', (endpointLabels) => {
+ const warnings: string[] = [];
const layout = compileChart(
+ lineSpec({ legend: { show: false }, endpointLabels, labels: { density: 'all' } }),
+ { ...SIZE, onWarn: (warning) => warnings.push(warning) },
+ );
+ const lines = layout.marks.filter((mark): mark is LineMark => mark.type === 'line');
+ expect(lines).toHaveLength(2);
+ expect(lines.every((line) => line.label === undefined)).toBe(true);
+ expect(layout.endpointLabels?.entries ?? []).toEqual([]);
+ expect(layout.legend.entries).toEqual([]);
+ expect(
+ warnings.filter((warning) => warning.includes('Both legend and endpointLabels')),
+ ).toEqual([
+ '[openchart a11y] Both legend and endpointLabels are disabled on a multi-series chart. Enable either to identify the series.',
+ ]);
+ });
+
+ it('warns once when annotation notes re-run the compile', () => {
+ const warnings: string[] = [];
+ compileChart(
lineSpec({
legend: { show: false },
endpointLabels: false,
- labels: { density: 'all' },
+ annotations: [
+ { type: 'text', x: multiSeries[0].date, y: multiSeries[0].value, text: 'One' },
+ { type: 'text', x: multiSeries[1].date, y: multiSeries[1].value, text: 'Two' },
+ ],
} as Partial),
- SIZE,
+ { ...SIZE, onWarn: (warning) => warnings.push(warning) },
);
- const lines = layout.marks.filter((m): m is LineMark => m.type === 'line');
- const labelled = lines.filter((l) => l.label);
+ expect(
+ warnings.filter((warning) => warning.includes('Both legend and endpointLabels')),
+ ).toHaveLength(1);
+ });
- expect(labelled.length).toBeGreaterThan(0);
- for (const line of labelled) {
- expect(line.label!.style.fill).toBe(line.stroke);
- }
+ it.each([
+ { legend: { show: true }, endpointLabels: false },
+ { legend: { show: false }, endpointLabels: true },
+ ])('does not warn while a series identification surface is enabled', (identification) => {
+ const warnings: string[] = [];
+ const layout = compileChart(lineSpec(identification), {
+ ...SIZE,
+ onWarn: (warning) => warnings.push(warning),
+ });
+ expect(layout.legend.entries.length + (layout.endpointLabels?.entries.length ?? 0)).toBe(2);
+ expect(warnings.some((warning) => warning.includes('Both legend and endpointLabels'))).toBe(
+ false,
+ );
+ });
+
+ it('does not demand a series identification surface for a single series', () => {
+ const warnings: string[] = [];
+ compileChart(
+ lineSpec({
+ data: multiSeries.filter((row) => row.series === 'North'),
+ legend: { show: false },
+ endpointLabels: false,
+ }),
+ { ...SIZE, onWarn: (warning) => warnings.push(warning) },
+ );
+ expect(warnings.some((warning) => warning.includes('Both legend and endpointLabels'))).toBe(
+ false,
+ );
});
});
@@ -222,7 +272,7 @@ describe('donut centerLabel', () => {
{ category: 'B', value: 60 },
],
encoding: {
- y: { field: 'value', type: 'quantitative' },
+ theta: { field: 'value', type: 'quantitative' },
color: { field: 'category', type: 'nominal' },
},
}) as ChartSpec;
diff --git a/packages/engine/src/__tests__/spec-sugar.test.ts b/packages/engine/src/__tests__/spec-sugar.test.ts
index 96f371f2..9bfde7b0 100644
--- a/packages/engine/src/__tests__/spec-sugar.test.ts
+++ b/packages/engine/src/__tests__/spec-sugar.test.ts
@@ -5,8 +5,8 @@
* Covers: data {values} unwrapping, top-level title/subtitle, bare value
* defs, channel-level legend, axis: null, scale.scheme resolution, theta as
* the arc value channel, count-without-field, VL sort forms, width/height
- * fixed sizing, strokeDash on line marks, and the exactly-one-warning
- * contract for v8-deprecated surface (radius/shape/href/order, $schema).
+ * fixed sizing, strokeDash on line marks, rejection of removed channels, and
+ * warning routing for retained sugar such as $schema.
*/
import type { LayerSpec, LineMark } from '@opendata-ai/openchart-core';
@@ -326,44 +326,39 @@ describe('theta on arc marks', () => {
expect(warned()).toEqual([]);
});
- it('warns once when only the deprecated y alias is used', () => {
- const layout = compileChart(
- {
- mark: 'arc',
- data,
- encoding: {
- y: { field: 'amount', type: 'quantitative' },
- color: { field: 'category', type: 'nominal' },
+ it('rejects y-only arc specs with the canonical theta replacement', () => {
+ expect(() =>
+ compileChart(
+ {
+ mark: 'arc',
+ data,
+ encoding: {
+ y: { field: 'amount', type: 'quantitative' },
+ color: { field: 'category', type: 'nominal' },
+ },
},
- },
- OPTIONS,
- );
- const arcs = layout.marks.filter((m) => m.type === 'arc');
- expect(arcs.length).toBe(2);
- const messages = warned().filter((m) => m.includes('encoding.y'));
- expect(messages).toHaveLength(1);
- expect(messages[0]).toContain('v8');
- expect(messages[0]).toContain('theta');
+ OPTIONS,
+ ),
+ ).toThrow(/encoding.y.*Use encoding.theta/);
+ expect(warned()).toEqual([]);
});
- it('prefers theta over y when both are present (theta is canonical)', () => {
- const layout = compileChart(
- {
- mark: 'arc',
- data,
- encoding: {
- y: { field: 'category', type: 'quantitative' },
- theta: { field: 'amount', type: 'quantitative' },
- color: { field: 'category', type: 'nominal' },
+ it('rejects a differing y when theta is present instead of silently dropping it', () => {
+ expect(() =>
+ compileChart(
+ {
+ mark: 'arc',
+ data,
+ encoding: {
+ y: { field: 'category', type: 'quantitative' },
+ theta: { field: 'amount', type: 'quantitative' },
+ color: { field: 'category', type: 'nominal' },
+ },
},
- },
- OPTIONS,
- );
- const arcs = layout.marks.filter((m) => m.type === 'arc');
- expect(arcs.length).toBe(2);
- expect(warned()).toEqual([
- '[openchart] encoding.theta and encoding.y are both set on arc; theta takes precedence and y was dropped.',
- ]);
+ OPTIONS,
+ ),
+ ).toThrow(/\/encoding\/y was removed in v9\. Remove encoding\.y/);
+ expect(warned()).toEqual([]);
});
it('does not warn when theta and y are identical', () => {
@@ -628,10 +623,10 @@ describe('strokeDash on line marks', () => {
});
// ---------------------------------------------------------------------------
-// Deprecation warnings (v8 clock)
+// Removed spec fields and retained sugar warnings
// ---------------------------------------------------------------------------
-describe('deprecation warnings', () => {
+describe('removed fields and retained warnings', () => {
const base = {
mark: 'bar',
data: [{ cat: 'A', value: 1 }],
@@ -646,17 +641,14 @@ describe('deprecation warnings', () => {
'shape',
'href',
'order',
- ] as const)('warns exactly once for the dead %s channel, naming the removal version', (channel) => {
- compileChart(
- {
- ...base,
- encoding: { ...base.encoding, [channel]: { field: 'value', type: 'quantitative' } },
- },
- OPTIONS,
- );
- const messages = warned().filter((m) => m.includes(`encoding.${channel}`));
- expect(messages).toHaveLength(1);
- expect(messages[0]).toContain('v8');
+ ] as const)('rejects the removed %s channel before sugar strips it', (channel) => {
+ const spec = {
+ ...base,
+ encoding: { ...base.encoding, [channel]: { field: 'value', type: 'quantitative' } },
+ };
+ expect(() => compileChart(spec, OPTIONS)).toThrow(`/encoding/${channel} was removed in v9`);
+ expect(() => expandSpecSugar(spec)).toThrow(`/encoding/${channel} was removed in v9`);
+ expect(warned()).toEqual([]);
});
it('warns once for $schema and strips it', () => {
@@ -669,17 +661,17 @@ describe('deprecation warnings', () => {
expect(warned().filter((m) => m.includes('$schema'))).toHaveLength(1);
});
- it("warns once for the deprecated 'rule' annotation type, naming the refline replacement", () => {
- compileChart(
- {
- ...base,
- annotations: [{ type: 'rule', y: 1, label: 'target' }],
- },
- OPTIONS,
- );
- const messages = warned().filter((m) => m.includes("annotation type 'rule'"));
- expect(messages).toHaveLength(1);
- expect(messages[0]).toContain("'refline'");
+ it("rejects the removed 'rule' annotation type with the refline replacement", () => {
+ expect(() =>
+ compileChart(
+ {
+ ...base,
+ annotations: [{ type: 'rule', y: 1, label: 'target' }],
+ },
+ OPTIONS,
+ ),
+ ).toThrow(/\/annotations\/0\/type.*Use refline/);
+ expect(warned()).toEqual([]);
});
it("does not warn for the canonical 'refline' annotation type", () => {
@@ -731,35 +723,24 @@ describe('layer specs', () => {
}
});
- it('emits each deprecation warning once per compile even across layer children', () => {
- compileLayer(
- {
- data: [
- { cat: 'A', value: 1 },
- { cat: 'B', value: 2 },
- ],
- layer: [
- {
- mark: 'bar',
- encoding: {
- x: { field: 'cat', type: 'nominal' },
- y: { field: 'value', type: 'quantitative' },
- href: { field: 'cat', type: 'nominal' },
- },
- },
- {
- mark: 'line',
- encoding: {
- x: { field: 'cat', type: 'ordinal' },
- y: { field: 'value', type: 'quantitative' },
- href: { field: 'cat', type: 'nominal' },
- },
- },
- ],
- } as unknown as LayerSpec,
- OPTIONS,
- );
- expect(warned().filter((m) => m.includes('encoding.href'))).toHaveLength(1);
+ it('reports each removed field with its authored layer path', () => {
+ const spec = {
+ data: [
+ { cat: 'A', value: 1 },
+ { cat: 'B', value: 2 },
+ ],
+ layer: ['bar', 'line'].map((mark) => ({
+ mark,
+ encoding: {
+ x: { field: 'cat', type: 'nominal' },
+ y: { field: 'value', type: 'quantitative' },
+ href: { field: 'cat', type: 'nominal' },
+ },
+ })),
+ };
+ expect(() => compileLayer(spec as LayerSpec, OPTIONS)).toThrow('/layer/0/encoding/href');
+ expect(() => compileLayer(spec as LayerSpec, OPTIONS)).toThrow('/layer/1/encoding/href');
+ expect(warned()).toEqual([]);
});
});
@@ -769,13 +750,12 @@ describe('layer specs', () => {
describe('options.onWarn warning sink', () => {
const deprecated = {
+ $schema: 'https://vega.github.io/schema/vega-lite/v5.json',
mark: 'bar',
data: [{ cat: 'A', value: 1 }],
encoding: {
x: { field: 'cat', type: 'nominal' },
y: { field: 'value', type: 'quantitative' },
- // A dead v8 channel: guaranteed to produce a deprecation warning.
- href: { field: 'value', type: 'quantitative' },
},
};
@@ -784,9 +764,9 @@ describe('options.onWarn warning sink', () => {
compileChart(deprecated, { ...OPTIONS, onWarn: (m) => sink.push(m) });
// The warning reached the sink...
- expect(sink.some((m) => m.includes('encoding.href'))).toBe(true);
+ expect(sink.some((m) => m.includes('$schema'))).toBe(true);
// ...and console.warn was NOT touched (the spy caught nothing).
- expect(warned().filter((m) => m.includes('encoding.href'))).toEqual([]);
+ expect(warned().filter((m) => m.includes('$schema'))).toEqual([]);
});
it('a no-op sink silences warnings entirely (e.g. SSR)', () => {
@@ -796,6 +776,6 @@ describe('options.onWarn warning sink', () => {
it('falls back to console.warn when no sink is provided', () => {
compileChart(deprecated, OPTIONS);
- expect(warned().some((m) => m.includes('encoding.href'))).toBe(true);
+ expect(warned().some((m) => m.includes('$schema'))).toBe(true);
});
});
diff --git a/packages/engine/src/__tests__/stroke-dash-series.test.ts b/packages/engine/src/__tests__/stroke-dash-series.test.ts
new file mode 100644
index 00000000..04502094
--- /dev/null
+++ b/packages/engine/src/__tests__/stroke-dash-series.test.ts
@@ -0,0 +1,111 @@
+import type { ChartSpec, LineMark } from '@opendata-ai/openchart-core';
+import { describe, expect, it } from 'vitest';
+import { compileChart } from '../compile';
+import { migrateSpecV8 } from '../migration/v8';
+
+const data = ['United States', 'Euro Area', 'China'].flatMap((country, i) =>
+ [2020, 2021, 2022].map((year) => ({ year: String(year), gdp: i * 3 + year - 2020, country })),
+);
+const dashSpec = (extra: Partial = {}): ChartSpec =>
+ ({
+ mark: 'line',
+ data,
+ encoding: {
+ x: { field: 'year', type: 'ordinal' },
+ y: { field: 'gdp', type: 'quantitative' },
+ strokeDash: { field: 'country', type: 'nominal' },
+ },
+ ...extra,
+ }) as ChartSpec;
+
+function compile(spec: ChartSpec, width = 1280) {
+ const warnings: string[] = [];
+ const layout = compileChart(spec, { width, height: 400, onWarn: (m) => warnings.push(m) });
+ const lines = layout.marks.filter((m): m is LineMark => m.type === 'line');
+ const legendEntries = 'entries' in layout.legend ? layout.legend.entries : [];
+ return { layout, lines, legendEntries, warnings };
+}
+const identificationWarning = (warnings: string[]) =>
+ warnings.filter((w) => w.includes('legend and endpointLabels are disabled'));
+
+describe('strokeDash-only multi-series lines', () => {
+ it('identifies each series in the endpoint column with its line color and dash', () => {
+ const { layout, lines, legendEntries, warnings } = compile(dashSpec());
+ const entries = layout.endpointLabels?.entries ?? [];
+ expect(entries.map((e) => e.seriesKey)).toEqual(['United States', 'Euro Area', 'China']);
+ for (const entry of entries) {
+ const line = lines.find((l) => l.seriesKey === entry.seriesKey)!;
+ expect(entry.color).toBe(line.stroke);
+ expect(entry.strokeDasharray).toBe(line.strokeDasharray);
+ }
+ expect(entries.find((e) => e.seriesKey === 'Euro Area')?.strokeDasharray).toBe('6 4');
+ // The endpoint column replaces the legend, as for color series.
+ expect(legendEntries).toEqual([]);
+ expect(identificationWarning(warnings)).toEqual([]);
+ });
+
+ it('falls back to a strokeDash legend with dashed line swatches', () => {
+ const { lines, legendEntries } = compile(dashSpec({ endpointLabels: false }));
+ expect(legendEntries.map((e) => [e.label, e.shape, e.strokeDasharray])).toEqual([
+ ['United States', 'line', undefined],
+ ['Euro Area', 'line', '6 4'],
+ ['China', 'line', '2 3'],
+ ]);
+ expect(new Set(legendEntries.map((e) => e.color))).toEqual(new Set([lines[0].stroke]));
+ });
+
+ it('uses the strokeDash legend at the compact breakpoint', () => {
+ const { layout, legendEntries } = compile(dashSpec(), 390);
+ expect(layout.endpointLabels?.entries ?? []).toEqual([]);
+ expect(legendEntries.map((e) => e.label)).toEqual(['United States', 'Euro Area', 'China']);
+ });
+
+ it('hides a toggled series without shifting the remaining dash patterns', () => {
+ const { lines, legendEntries } = compile(
+ dashSpec({ hiddenSeries: ['Euro Area'], endpointLabels: false }),
+ );
+ expect(lines.map((l) => [l.seriesKey, l.strokeDasharray])).toEqual([
+ ['United States', undefined],
+ ['China', '2 3'],
+ ]);
+ expect(legendEntries.find((e) => e.label === 'Euro Area')?.active).toBe(false);
+ });
+
+ it('warns only when the author turns both identifiers off', () => {
+ const { warnings } = compile(dashSpec({ legend: { show: false }, endpointLabels: false }));
+ expect(identificationWarning(warnings)).toHaveLength(1);
+ });
+
+ it('gives detail-only groups no legend or endpoint column (Vega-Lite detail)', () => {
+ const spec = dashSpec();
+ const encoding = { ...spec.encoding, detail: { field: 'country', type: 'nominal' } };
+ delete (encoding as { strokeDash?: unknown }).strokeDash;
+ const { layout, legendEntries, warnings } = compile({ ...spec, encoding } as ChartSpec);
+ expect(layout.endpointLabels?.entries ?? []).toEqual([]);
+ expect(legendEntries).toEqual([]);
+ expect(identificationWarning(warnings)).toEqual([]);
+ });
+});
+
+describe('migrateSpecV8 strokeDash series labels', () => {
+ it.each([
+ {},
+ { legend: { show: false }, endpointLabels: false },
+ ])('keeps v8 end-of-line labels as label-only endpoint labels (%j)', (extra) => {
+ const { spec, repairs } = migrateSpecV8(dashSpec(extra as Partial));
+ expect(spec.endpointLabels).toEqual({ content: 'label', showMarker: false });
+ expect(repairs.map((r) => r.path)).toContain('/endpointLabels');
+ expect(migrateSpecV8(spec).repairs).toEqual([]);
+ const entries = compile(spec).layout.endpointLabels?.entries ?? [];
+ expect(entries.map((e) => [e.labelLines.join(' '), e.value, e.marker])).toEqual([
+ ['United States', '', undefined],
+ ['Euro Area', '', undefined],
+ ['China', '', undefined],
+ ]);
+ });
+
+ it('leaves dash series alone when v8 drew no labels', () => {
+ const source = dashSpec({ labels: { density: 'none' } });
+ expect(migrateSpecV8(source).spec).not.toHaveProperty('endpointLabels');
+ });
+});
diff --git a/packages/engine/src/__tests__/theme-precedence.test.ts b/packages/engine/src/__tests__/theme-precedence.test.ts
new file mode 100644
index 00000000..f80bd849
--- /dev/null
+++ b/packages/engine/src/__tests__/theme-precedence.test.ts
@@ -0,0 +1,112 @@
+import type { ChartSpec, LayerSpec } from '@opendata-ai/openchart-core';
+import { describe, expect, it } from 'vitest';
+import { compileChart, compileLayer } from '../compile';
+
+const leaf: ChartSpec = {
+ mark: 'line',
+ data: [
+ { x: 0, y: 1 },
+ { x: 1, y: 3 },
+ ],
+ encoding: { x: { field: 'x', type: 'ordinal' }, y: { field: 'y', type: 'quantitative' } },
+ chrome: { title: 'Title' },
+};
+const point = (fill: string) => ({ ...leaf, mark: 'point' as const, theme: themeWith(fill) });
+const themeWith = (fill: string) => ({ colors: { categorical: [fill] } });
+const pointFills = (layout: { marks: { type: string }[] }) => [
+ ...new Set(
+ layout.marks.filter((m) => m.type === 'point').map((m) => (m as { fill: string }).fill),
+ ),
+];
+
+describe('theme layer precedence', () => {
+ for (const independent of [false, true]) {
+ it(`takes root chrome from the layer theme, not leaf 0, with ${independent ? 'independent' : 'shared'} axes`, () => {
+ const spec: LayerSpec = {
+ layer: [
+ { ...leaf, theme: { fonts: { sizes: { title: 31 } }, colors: { text: '#123456' } } },
+ leaf,
+ ],
+ theme: { fonts: { sizes: { title: 20, small: 14 } }, colors: { background: '#faf8f5' } },
+ ...(independent ? { resolve: { scale: { y: 'independent' as const } } } : {}),
+ };
+ const result = compileLayer(spec, {
+ width: 600,
+ height: 400,
+ theme: { fonts: { sizes: { axisTick: 17 } } },
+ });
+ // Whole-object choice: none of leaf 0's theme reaches the root.
+ expect(result.theme.fonts.sizes).toMatchObject({ title: 20, small: 14, axisTick: 17 });
+ expect(result.theme.colors.background).toBe('#faf8f5');
+ expect(result.theme.colors.text).not.toBe('#123456');
+ });
+ }
+
+ it('falls back to the leaf 0 theme for the root when the layer has none', () => {
+ const result = compileLayer(
+ { layer: [{ ...leaf, theme: { fonts: { sizes: { title: 31 } } } }, leaf] },
+ { width: 600, height: 400, theme: { fonts: { sizes: { axisTick: 17 } } } },
+ );
+ expect(result.theme.fonts.sizes).toMatchObject({ title: 31, axisTick: 17 });
+ });
+
+ it("deep-merges each leaf theme over the layer theme for that leaf's marks", () => {
+ const layerTheme = { colors: { categorical: ['#111111'] }, fonts: { sizes: { title: 20 } } };
+ const result = compileLayer(
+ {
+ theme: layerTheme,
+ layer: [point('#00ff00'), point('#ff0000'), { ...leaf, mark: 'point' as const }],
+ },
+ { width: 600, height: 400 },
+ );
+ // Leaf 0 and leaf N both use their own palette; a theme-less leaf inherits the layer's.
+ expect(pointFills(result)).toEqual(['#00ff00', '#ff0000', '#111111']);
+ expect(result.theme.colors.categorical[0]).toBe('#111111');
+ expect(result.theme.fonts.sizes.title).toBe(20);
+ });
+
+ it('deep-merges each dual-axis leaf theme over the layer theme for its marks and axis', () => {
+ const tick = (size: number) => ({ fonts: { sizes: { axisTick: size } } });
+ const result = compileLayer(
+ {
+ theme: { ...themeWith('#111111'), fonts: { sizes: { axisTick: 15, title: 20 } } },
+ resolve: { scale: { y: 'independent' } },
+ layer: [
+ { ...point('#00ff00'), theme: { ...themeWith('#00ff00'), ...tick(9) } },
+ point('#ff0000'),
+ ],
+ },
+ { width: 600, height: 400 },
+ );
+ expect(pointFills(result)).toEqual(['#00ff00', '#ff0000']);
+ expect(result.axes.y?.tickLabelStyle.fontSize).toBe(9);
+ // Leaf 1 sets no axisTick, so its axis inherits the layer's.
+ expect(result.axes.y2?.tickLabelStyle.fontSize).toBe(15);
+ // Root chrome stays with the layer theme.
+ expect(result.theme.fonts.sizes.title).toBe(20);
+ expect(result.theme.colors.categorical[0]).toBe('#111111');
+ });
+
+ it('cascades the layer theme onto a leaf for keys the leaf does not set', () => {
+ const result = compileLayer(
+ {
+ theme: { colors: { categorical: ['#111111'] } },
+ layer: [{ ...leaf, mark: 'point' as const, theme: { fonts: { sizes: { small: 16 } } } }],
+ },
+ { width: 600, height: 400 },
+ );
+ expect(pointFills(result)).toEqual(['#111111']);
+ });
+ it('honors host watermark overrides even for sparkline and breakpoint defaults', () => {
+ expect(
+ compileChart({ ...leaf, watermark: true }, { width: 600, height: 400, watermark: false })
+ .watermark,
+ ).toBe(false);
+ expect(
+ compileChart(
+ { ...leaf, watermark: false, display: 'sparkline' },
+ { width: 600, height: 180, watermark: true },
+ ).watermark,
+ ).toBe(true);
+ });
+});
diff --git a/packages/engine/src/__tests__/track-a-wrapping-and-dual-axis.test.ts b/packages/engine/src/__tests__/track-a-wrapping-and-dual-axis.test.ts
index bb071504..006b59d2 100644
--- a/packages/engine/src/__tests__/track-a-wrapping-and-dual-axis.test.ts
+++ b/packages/engine/src/__tests__/track-a-wrapping-and-dual-axis.test.ts
@@ -68,7 +68,7 @@ describe('wrapped annotation notes', () => {
expect(notes.every((note) => (note.lines?.length ?? 0) > 1)).toBe(true);
for (const note of notes) {
expect(note.lines!.join(' ').replaceAll(' ', '')).toBe(
- `${note.index}. ${note.text}`.replaceAll(' ', ''),
+ `${note.index}. ${note.text}${note.subtitle ?? ''}`.replaceAll(' ', ''),
);
for (const line of note.lines!)
expect(
diff --git a/packages/engine/src/annotations/__tests__/compute.test.ts b/packages/engine/src/annotations/__tests__/compute.test.ts
index 573bf008..8eb0b56e 100644
--- a/packages/engine/src/annotations/__tests__/compute.test.ts
+++ b/packages/engine/src/annotations/__tests__/compute.test.ts
@@ -1,9 +1,10 @@
import type { Annotation, LayoutStrategy, Rect } from '@opendata-ai/openchart-core';
import type { ScaleBand, ScalePoint } from 'd3-scale';
-import { describe, expect, it } from 'vitest';
+import { describe, expect, it, vi } from 'vitest';
import type { NormalizedChartSpec } from '../../compiler/types';
import { computeScales } from '../../layout/scales';
import { computeAnnotations } from '../compute';
+import { ConnectorCrossings } from '../connector-crossings';
import { heuristicMeasure as annotationMeasureForTest } from '../geometry';
import { normalizeObstacles as normalizeObstaclesForTest } from '../placement';
import { resolvePosition } from '../position';
@@ -311,7 +312,7 @@ describe('computeAnnotations', () => {
expect(annotations[0].label!.text).toBe('Recession');
});
- it('range label defaults to 11px / weight 500', () => {
+ it('range label defaults to 12px / weight 500', () => {
const spec = makeSpec([
{ type: 'range', x1: '2020-01-01', x2: '2021-01-01', label: 'Period' },
]);
@@ -326,7 +327,7 @@ describe('computeAnnotations', () => {
measure: annotationMeasureForTest,
});
- expect(annotations[0].label!.style.fontSize).toBe(11);
+ expect(annotations[0].label!.style.fontSize).toBe(12);
expect(annotations[0].label!.style.fontWeight).toBe(500);
});
@@ -845,7 +846,7 @@ describe('computeAnnotations', () => {
expect(annotations[0].strokeDasharray).toBeDefined();
});
- it('refline label defaults to fontSize 11', () => {
+ it('refline label defaults to fontSize 12', () => {
const spec = makeSpec([{ type: 'refline', x: '2020-06-01', label: 'Event' }]);
const scales = computeScales(spec, chartArea, spec.data);
const annotations = computeAnnotations(spec, {
@@ -858,7 +859,7 @@ describe('computeAnnotations', () => {
measure: annotationMeasureForTest,
});
- expect(annotations[0].label!.style.fontSize).toBe(11);
+ expect(annotations[0].label!.style.fontSize).toBe(12);
});
it('refline label honors fontSize and fontWeight overrides', () => {
@@ -2403,3 +2404,73 @@ describe('computeAnnotations', () => {
});
});
});
+
+describe('default callout wrap width', () => {
+ it('keeps a usable minimum width on tiny plots instead of wrapping per character', () => {
+ const tiny: Rect = { x: 10, y: 10, width: 24, height: 200 };
+ const spec = makeSpec([
+ { type: 'text', x: '2020-01-01', y: 20, text: 'Peak demand', offset: { dx: 0, dy: -40 } },
+ ]);
+ const scales = computeScales(spec, tiny, spec.data);
+ const [annotation] = computeAnnotations(spec, {
+ scales,
+ chartArea: tiny,
+ strategy: fullStrategy,
+ isDark: false,
+ obstacles: [],
+ svg: { width: 60, height: 240 },
+ measure: annotationMeasureForTest,
+ padding: 16,
+ });
+ const lines = annotation.label!.lines ?? [annotation.label!.text];
+ expect(lines.every((line) => line.length > 1)).toBe(true);
+ });
+});
+
+describe('auto placement order and scoring', () => {
+ const run = (annotations: Annotation[]) => {
+ const spec = makeSpec(annotations);
+ return computeAnnotations(spec, {
+ scales: computeScales(spec, chartArea, spec.data),
+ chartArea,
+ strategy: fullStrategy,
+ isDark: false,
+ obstacles: [],
+ svg: { width: 600, height: 340 },
+ measure: annotationMeasureForTest,
+ });
+ };
+ const at = (layout: ReturnType, text: string) =>
+ layout.find((a) => a.label?.text === text)!.label!;
+
+ it('lets a later, higher-priority callout claim its preferred position first', () => {
+ const alone = at(run([{ type: 'text', x: '2020-01-01', y: 20, text: 'Urgent' }]), 'Urgent');
+ const both = run([
+ { type: 'text', x: '2020-01-01', y: 20, text: 'Routine', priority: 2 },
+ { type: 'text', x: '2020-01-01', y: 20, text: 'Urgent', priority: 1 },
+ ]);
+ expect([at(both, 'Urgent').x, at(both, 'Urgent').y]).toEqual([alone.x, alone.y]);
+ const routine = at(both, 'Routine');
+ expect([routine.x, routine.y]).not.toEqual([alone.x, alone.y]);
+ });
+
+ it('moves an auto callout whose preferred leader would cross a placed callout', () => {
+ const annotations: Annotation[] = [
+ { type: 'text', x: '2020-01-01', y: 20, text: 'Pinned note', offset: { dx: 0, dy: 80 } },
+ { type: 'text', x: '2020-03-01', y: 15, text: 'Auto note' },
+ ];
+ const scored = run(annotations);
+ const spy = vi.spyOn(ConnectorCrossings.prototype, 'score').mockReturnValue(0);
+ const unscored = run(annotations);
+ spy.mockRestore();
+
+ const pinned = new ConnectorCrossings();
+ const placed = scored.find((a) => a.label?.text === 'Pinned note')!;
+ pinned.addBox({ ...placed.bounds!, kind: 'annotation' });
+ pinned.addConnector(placed.label!.connector);
+ const crossings = (layout: ReturnType) =>
+ pinned.score(at(layout, 'Auto note').connector);
+ expect(crossings(unscored)).toBeGreaterThan(0);
+ expect(crossings(scored)).toBe(0);
+ });
+});
diff --git a/packages/engine/src/annotations/__tests__/connector-control.test.ts b/packages/engine/src/annotations/__tests__/connector-control.test.ts
index 31377f38..c67ffd48 100644
--- a/packages/engine/src/annotations/__tests__/connector-control.test.ts
+++ b/packages/engine/src/annotations/__tests__/connector-control.test.ts
@@ -25,16 +25,27 @@ describe('engine connector control', () => {
it('omitted and zero bend produce exactly the same control point', () => {
const plain = refreshConnector(connector, bounds, 0)!;
const zero = refreshConnector({ ...connector, bend: 0 }, bounds, 0)!;
- expect(plain.control).toEqual(automaticControlPoint(plain.from, plain.to, plain.exit));
+ // The curve is shaped to the data point, not the pulled-back tip.
+ expect(plain.control).toEqual(automaticControlPoint(plain.from, plain.endpoint!, plain.exit));
expect(zero.control).toEqual(plain.control);
});
it.each([-1, -0.5, 0.5, 1])('displaces by the directed chord normal for bend %s', (bend) => {
const base = refreshConnector(connector, bounds, 0)!;
const bent = refreshConnector({ ...connector, bend }, bounds, 0)!;
- expect(bent.control!.x).toBeCloseTo(base.control!.x - bend * (base.from.y - base.to.y));
- expect(bent.control!.y).toBeCloseTo(base.control!.y + bend * (base.from.x - base.to.x));
+ const end = connector.endpoint!;
+ expect(bent.control!.x).toBeCloseTo(base.control!.x - bend * (base.from.y - end.y));
+ expect(bent.control!.y).toBeCloseTo(base.control!.y + bend * (base.from.x - end.x));
expect(bent.from).toEqual(base.from);
- expect(bent.to).toEqual(base.to);
+ });
+ it.each([-1, -0.5, 0, 0.5, 1])('ends aimed at the marker for bend %s', (bend) => {
+ const c = refreshConnector({ ...connector, bend }, bounds, 4)!;
+ const end = connector.endpoint!;
+ // The tail tangent (control -> to) runs through the data point.
+ const cross =
+ (c.to.x - c.control!.x) * (end.y - c.control!.y) -
+ (c.to.y - c.control!.y) * (end.x - c.control!.x);
+ expect(Math.abs(cross)).toBeLessThan(1e-6);
+ expect(Math.hypot(end.x - c.to.x, end.y - c.to.y)).toBeCloseTo(6);
});
it('recomputes control after a block move and preserves authored attachment offset', () => {
const original = refreshConnector(
diff --git a/packages/engine/src/annotations/__tests__/connector-crossings.test.ts b/packages/engine/src/annotations/__tests__/connector-crossings.test.ts
new file mode 100644
index 00000000..bf3c446e
--- /dev/null
+++ b/packages/engine/src/annotations/__tests__/connector-crossings.test.ts
@@ -0,0 +1,36 @@
+import type { ResolvedLabel } from '@opendata-ai/openchart-core';
+import { expect, it } from 'vitest';
+import { ConnectorCrossings } from '../connector-crossings';
+
+const connector: NonNullable = {
+ from: { x: 0, y: 0 },
+ to: { x: 100, y: 0 },
+ endpoint: { x: 110, y: 0 },
+ style: 'straight',
+};
+it('penalizes unrelated labels and leaders while excluding contact with its target', () => {
+ const index = new ConnectorCrossings();
+ index.addBox({ x: 95, y: -5, width: 20, height: 10, kind: 'mark' });
+ expect(index.score(connector)).toBe(0);
+ index.addBox({ x: 45, y: -5, width: 10, height: 10, kind: 'annotation' });
+ expect(index.score(connector)).toBeGreaterThan(0);
+ const leaders = new ConnectorCrossings();
+ leaders.addConnector({ ...connector, from: { x: 50, y: -50 }, to: { x: 50, y: 50 } });
+ expect(leaders.score(connector)).toBeGreaterThan(0);
+});
+it('scores the rendered curve instead of its straight chord and stays deterministic', () => {
+ const index = new ConnectorCrossings();
+ index.addBox({ x: 40, y: 45, width: 20, height: 10, kind: 'annotation' });
+ expect(index.score(connector)).toBe(0);
+ const curve = { ...connector, style: 'curve' as const, control: { x: 50, y: 100 } };
+ expect(index.score(curve)).toBeGreaterThan(0);
+ expect(index.score(curve)).toBe(index.score(curve));
+});
+
+it('bounds grid work for extreme off-plot coordinates without missing crossings', () => {
+ const index = new ConnectorCrossings();
+ index.addBox({ x: -1e12, y: -1, width: 2e12, height: 2, kind: 'annotation' });
+ expect(index.score(connector)).toBeGreaterThan(0);
+ const enormous = { ...connector, from: { x: -1e12, y: 0 }, to: { x: 1e12, y: 0 } };
+ expect(index.score(enormous)).toBeGreaterThan(0);
+});
diff --git a/packages/engine/src/annotations/__tests__/data-label-connectors.test.ts b/packages/engine/src/annotations/__tests__/data-label-connectors.test.ts
new file mode 100644
index 00000000..264d0a74
--- /dev/null
+++ b/packages/engine/src/annotations/__tests__/data-label-connectors.test.ts
@@ -0,0 +1,143 @@
+import type {
+ ChartLayout,
+ ChartSpec,
+ Point,
+ Rect,
+ ResolvedLabel,
+ TextAnnotation,
+} from '@opendata-ai/openchart-core';
+import { computeLabelBounds } from '@opendata-ai/openchart-core';
+import { describe, expect, it } from 'vitest';
+import { compileChart } from '../../compile';
+import { interpolateAnnotationGeometry } from '../interpolate';
+
+type Connector = NonNullable;
+
+const payrolls = (
+ [
+ ['Jan', 353],
+ ['Feb', 275],
+ ['Mar', 303],
+ ['Apr', 175],
+ ['May', 272],
+ ['Jun', 206],
+ ['Jul', 114],
+ ['Aug', 142],
+ ['Sep', 254],
+ ['Oct', 12],
+ ['Nov', 227],
+ ['Dec', 256],
+ ] as const
+).map(([month, jobs]) => ({ month, jobs }));
+
+function columns(annotation: TextAnnotation, labels = true): ChartSpec {
+ return {
+ mark: 'bar',
+ data: payrolls,
+ encoding: {
+ x: { field: 'month', type: 'nominal' },
+ y: { field: 'jobs', type: 'quantitative' },
+ },
+ labels: labels ? { density: 'all', format: ',.0f' } : { density: 'none' },
+ annotations: [annotation],
+ animation: false,
+ };
+}
+
+function dataLabelBoxes(layout: ChartLayout): Rect[] {
+ return layout.marks.flatMap((mark) =>
+ 'label' in mark && mark.label?.visible ? [computeLabelBounds(mark.label)] : [],
+ );
+}
+
+/** Dense samples along the drawn connector: the chord, or the quadratic the renderer draws. */
+function samples(connector: Connector): Point[] {
+ const { from, to, control } = connector;
+ const points: Point[] = [];
+ for (let i = 0; i <= 400; i++) {
+ const t = i / 400;
+ const u = 1 - t;
+ points.push(
+ connector.style === 'curve' && control
+ ? {
+ x: u * u * from.x + 2 * u * t * control.x + t * t * to.x,
+ y: u * u * from.y + 2 * u * t * control.y + t * t * to.y,
+ }
+ : { x: from.x + (to.x - from.x) * t, y: from.y + (to.y - from.y) * t },
+ );
+ }
+ return points;
+}
+
+function crossedLabels(connector: Connector, boxes: Rect[]): Rect[] {
+ return boxes.filter((box) =>
+ samples(connector).some(
+ (p) => p.x >= box.x && p.x <= box.x + box.width && p.y >= box.y && p.y <= box.y + box.height,
+ ),
+ );
+}
+
+const story: TextAnnotation = {
+ type: 'text',
+ x: 'Oct',
+ y: 12,
+ text: 'Hurricanes and strikes\nstalled October hiring',
+ anchor: 'top',
+ offset: { dy: -170 },
+ connector: true,
+};
+
+describe('annotation connectors and data labels', () => {
+ it('stops an authored leader short of the value label it would strike through', () => {
+ const layout = compileChart(columns(story), { width: 990, height: 450 });
+ const connector = layout.annotations[0].label?.connector;
+ expect(connector).toBeDefined();
+ expect(crossedLabels(connector!, dataLabelBoxes(layout))).toEqual([]);
+ // The leader still reaches down to the value label: it is shortened, not dropped.
+ expect(connector!.to.y).toBeGreaterThan(connector!.from.y + 100);
+
+ // The authored offset wins: the block sits exactly where it does without labels.
+ const unlabeled = compileChart(columns(story, false), { width: 990, height: 450 });
+ expect(layout.annotations[0].bounds).toEqual(unlabeled.annotations[0].bounds);
+ expect(layout.annotations[0].dot).toEqual(unlabeled.annotations[0].dot);
+ });
+
+ it.each([
+ ['straight', true],
+ ['curve', 'curve'],
+ ] as const)('keeps a default-placed %s leader off every value label', (_, connector) => {
+ const annotation: TextAnnotation = {
+ type: 'text',
+ x: 'Jul',
+ y: 114,
+ text: 'A notable callout\nwith two lines',
+ connector,
+ };
+ const layout = compileChart(columns(annotation), { width: 400, height: 450 });
+ const resolved = layout.annotations[0].label?.connector;
+ expect(resolved).toBeDefined();
+ expect(crossedLabels(resolved!, dataLabelBoxes(layout))).toEqual([]);
+ });
+
+ it('nudges an anchored block to a spot whose leader clears the value labels', () => {
+ const annotation: TextAnnotation = {
+ type: 'text',
+ x: 'Mar',
+ y: 303,
+ text: 'A much longer single line callout',
+ anchor: 'top',
+ connector: true,
+ };
+ const layout = compileChart(columns(annotation), { width: 700, height: 450 });
+ const resolved = layout.annotations[0].label?.connector;
+ expect(resolved).toBeDefined();
+ expect(crossedLabels(resolved!, dataLabelBoxes(layout))).toEqual([]);
+ });
+
+ it('keeps the stop when geometry is rebuilt mid-transition', () => {
+ const layout = compileChart(columns(story), { width: 990, height: 450 });
+ const annotation = layout.annotations[0];
+ const frame = interpolateAnnotationGeometry(annotation, annotation, 0.5);
+ expect(crossedLabels(frame.label!.connector!, dataLabelBoxes(layout))).toEqual([]);
+ });
+});
diff --git a/packages/engine/src/annotations/__tests__/edit-context.test.ts b/packages/engine/src/annotations/__tests__/edit-context.test.ts
index 3a7162b2..025a562c 100644
--- a/packages/engine/src/annotations/__tests__/edit-context.test.ts
+++ b/packages/engine/src/annotations/__tests__/edit-context.test.ts
@@ -168,9 +168,9 @@ describe('resolved annotation editing', () => {
layer: [{ layer: [{ ...editSpec, annotations: [bad] }] }],
});
expect(result.valid).toBe(false);
- expect(result.errors.map((error) => error.path)).toContain('annotations[0].connector.bend');
+ expect(result.errors.map((error) => error.path)).toContain('/annotations/0/connector/bend');
expect(result.errors.map((error) => error.path)).toContain(
- 'layer[0].layer[0].annotations[0].connector.bend',
+ '/layer/0/layer/0/annotations/0/connector/bend',
);
});
it('preserves the existing null connector fallback without throwing a warning error', () => {
@@ -194,7 +194,7 @@ describe('resolved annotation editing', () => {
],
});
expect(result.valid).toBe(false);
- expect(result.errors.map((error) => error.path)).toContain('annotations[0].connector.bend');
+ expect(result.errors.map((error) => error.path)).toContain('/annotations/0/connector/bend');
});
it('freezes the original layout and source during a gesture', () => {
const spec = structuredClone(editSpec);
diff --git a/packages/engine/src/annotations/__tests__/endpoint-marker-dot.test.ts b/packages/engine/src/annotations/__tests__/endpoint-marker-dot.test.ts
new file mode 100644
index 00000000..62fabebc
--- /dev/null
+++ b/packages/engine/src/annotations/__tests__/endpoint-marker-dot.test.ts
@@ -0,0 +1,52 @@
+import type { ChartSpec, TextAnnotation } from '@opendata-ai/openchart-core';
+import { describe, expect, it } from 'vitest';
+import { compileChart } from '../../compile';
+
+const data = ['2022-01-01', '2023-01-01', '2024-01-01'].flatMap((year, i) => [
+ { year, value: 100 + i * 50, series: 'A' },
+ { year, value: 80 + i * 10, series: 'B' },
+]);
+
+const spec = (annotation: TextAnnotation): ChartSpec => ({
+ mark: 'line',
+ data,
+ encoding: {
+ x: { field: 'year', type: 'temporal' },
+ y: { field: 'value', type: 'quantitative' },
+ color: { field: 'series', type: 'nominal' },
+ },
+ legend: { show: false },
+ endpointLabels: { content: 'label', showMarker: true },
+ annotations: [annotation],
+ animation: false,
+});
+
+const callout: TextAnnotation = {
+ type: 'text',
+ x: '2024-01-01',
+ y: 200,
+ text: 'A reaches 200',
+ anchor: 'left',
+ offset: { dx: -12, dy: -24 },
+ connector: true,
+};
+
+describe('annotation dot on an endpoint marker', () => {
+ it('draws as the marker: same center and colors', () => {
+ const layout = compileChart(spec(callout), { width: 800, height: 400 });
+ const marker = layout.endpointLabels!.entries.find((e) => e.seriesKey === 'A')!.marker!;
+ const dot = layout.annotations[0].dot!;
+ expect(dot.x).toBeCloseTo(marker.x, 6);
+ expect(dot.y).toBeCloseTo(marker.y, 6);
+ expect(dot.stroke).toBe(marker.stroke);
+ expect(dot.fill).toBe(marker.fill);
+ });
+
+ it('keeps an authored dot style', () => {
+ const layout = compileChart(spec({ ...callout, dot: { stroke: '#ff0000' } }), {
+ width: 800,
+ height: 400,
+ });
+ expect(layout.annotations[0].dot!.stroke).toBe('#ff0000');
+ });
+});
diff --git a/packages/engine/src/annotations/__tests__/layout-opt-ins.test.ts b/packages/engine/src/annotations/__tests__/layout-opt-ins.test.ts
index 5d02d980..9cb28862 100644
--- a/packages/engine/src/annotations/__tests__/layout-opt-ins.test.ts
+++ b/packages/engine/src/annotations/__tests__/layout-opt-ins.test.ts
@@ -58,11 +58,12 @@ describe('annotation text layout opt-ins', () => {
expect(a.subtitle!.style.fontSize).toBe(15);
expect(a.bounds!.width).toBeLessThanOrEqual(85);
});
- it('null and omission reproduce identical layout', () => {
+ it('null explicitly disables the v9 wrapping default', () => {
const compile = (a: TextAnnotation) =>
compileChart({ ...base, annotations: [a] }, { width: 600, height: 500 }).annotations;
- expect(compile({ ...annotation, maxWidth: null })).toEqual(compile(annotation));
- expect(compile({ ...annotation, subtitleFontSize: 11 })).toEqual(compile(annotation));
+ expect(compile({ ...annotation, maxWidth: null })[0].label?.lines).toBeUndefined();
+ expect(compile(annotation)[0].label!.lines!.length).toBeGreaterThan(1);
+ expect(compile({ ...annotation, subtitleFontSize: 12 })).toEqual(compile(annotation));
});
});
@@ -94,7 +95,9 @@ describe('compact annotation limit', () => {
{ width, height: 700 },
);
expect(compile(400, 0).annotations).toEqual(compile(400).annotations);
- expect(compile(390, null).annotations).toEqual(compile(390).annotations);
+ expect(
+ compile(390).annotations.filter((a) => !a.footnoteIndex && a.id !== 'pinned').length,
+ ).toBeLessThanOrEqual(1);
});
it('does not override D15 hide-all', () => {
const result = compileChart(
@@ -155,7 +158,7 @@ it('resolves authored annotation tokens and keeps explicit ink authoritative', (
anchor: 'left',
offset: { dx: -60, dy: -100 },
fill: '#aabbcc',
- connector: 'curve',
+ connector: { type: 'curve', arrow: true },
},
],
},
@@ -228,7 +231,7 @@ it.each([
it.each([
false,
true,
-])('preserves compiled rule/range defaults while honoring tokens (dark=%s)', (darkMode) => {
+])('applies v9 rule/range defaults while honoring tokens (dark=%s)', (darkMode) => {
const annotations: ChartSpec['annotations'] = [
{ type: 'refline', id: 'rule', y: 3, label: 'Rule' },
{ type: 'range', id: 'range', x1: 1, x2: 2, label: 'Range' },
@@ -239,11 +242,10 @@ it.each([
{ ...annotations[1], fill: '#000000' },
];
const defaults = compileChart({ ...base, annotations }, options);
- expect(defaults.theme.colors.annotationRule).toBe('#666666');
- expect(defaults.theme.colors.annotationRangeFill).toBe('#000000');
- expect(defaults.annotations).toEqual(
- compileChart({ ...base, annotations: legacyInk }, options).annotations,
- );
+ expect(defaults.theme.colors.annotationRule).toBe(darkMode ? '#9ca3af' : '#888888');
+ expect(defaults.theme.colors.annotationRangeFill).toBe('#f0c040');
+ expect(defaults.annotations[0].stroke).toBe(defaults.theme.colors.annotationRule);
+ expect(defaults.annotations[1].fill).toBe('#f0c040');
const theme = {
colors: {
annotationRule: { light: '#445566', dark: '#aabbcc' },
diff --git a/packages/engine/src/annotations/__tests__/reference-placement.test.ts b/packages/engine/src/annotations/__tests__/reference-placement.test.ts
index c3d606b9..30b8ad3c 100644
--- a/packages/engine/src/annotations/__tests__/reference-placement.test.ts
+++ b/packages/engine/src/annotations/__tests__/reference-placement.test.ts
@@ -131,7 +131,7 @@ it('auto avoids both occupied ends by sliding while retaining the authored side'
a,
result.area,
{ x: 0, y: 0, width: 640, height: 600 },
- { top: 0, right: 0, labelWidth: 160 },
+ { top: 0, right: 0, labelWidth: 160, labels: [], rows: [] },
{},
heuristicMeasure,
result.theme.colors,
@@ -310,3 +310,83 @@ it.each([
}
}
});
+
+describe('vertical outside reference lanes', () => {
+ const series: ChartSpec = {
+ mark: 'line',
+ data: Array.from({ length: 20 }, (_, i) => ({
+ x: `2020-01-${String(i + 1).padStart(2, '0')}`,
+ y: i,
+ })),
+ chrome: { title: 'Title here' },
+ encoding: { x: { field: 'x', type: 'temporal' }, y: { field: 'y', type: 'quantitative' } },
+ };
+ const label = 'A fairly long reference label that will wrap across lines';
+ const rules: RefLineAnnotation[] = [4, 10, 16].map((day, i) => ({
+ id: `r${i}`,
+ type: 'refline',
+ x: `2020-01-${String(day).padStart(2, '0')}`,
+ label,
+ labelPlacement: 'outside',
+ }));
+
+ it('reserves at least the rendered height of the wrapped labels at the theme small size', () => {
+ const plain = compileChart(series, { width: 600, height: 600 });
+ const layout = compileChart(
+ { ...series, annotations: rules },
+ { width: 600, height: 600, onWarn: () => {} },
+ );
+ const blocks = layout.annotations.map((a) => a.label!.bounds!);
+ // The three rules are far enough apart to share one row: the lane is the
+ // tallest block plus its 8px gap, and 24px separates it from the plot.
+ const rendered = Math.max(...blocks.map((b) => b.height)) + 8 + 24;
+ expect(layout.annotations[0].label!.style.fontSize).toBe(12);
+ expect(layout.area.y - plain.area.y).toBeGreaterThanOrEqual(rendered - 0.01);
+ for (const b of blocks) {
+ expect(b.y).toBeCloseTo(blocks[0].y);
+ expect(b.y + b.height).toBeLessThanOrEqual(layout.area.y);
+ }
+ });
+
+ it('never positions lanes off-canvas in a small figure', () => {
+ for (const height of [300, 400]) {
+ const layout = compileChart(
+ { ...series, annotations: rules },
+ { width: 380, height, onWarn: () => {} },
+ );
+ for (const a of layout.annotations) {
+ const b = a.label!.bounds!;
+ expect(b.y).toBeGreaterThanOrEqual(0);
+ expect(b.y + b.height).toBeLessThanOrEqual(height);
+ }
+ }
+ });
+});
+
+describe('partially admitted top reference lane', () => {
+ it('falls labels the lane cannot hold back inside, clear of the title, with one warning', () => {
+ const warnings: string[] = [];
+ const layout = compileChart(
+ {
+ ...base,
+ chrome: { title: 'Quarterly revenue', subtitle: 'Indexed to the first quarter' },
+ annotations: Array.from({ length: 4 }, (_, i) => ({
+ type: 'refline' as const,
+ x: 1 + i,
+ label: `Policy change ${i} announced in the spring`,
+ labelPlacement: 'outside' as const,
+ })),
+ },
+ { width: 640, height: 360, onWarn: (message) => warnings.push(message) },
+ );
+ // The lane yields before chrome: the subtitle stays.
+ expect(layout.chrome.subtitle).toBeDefined();
+ const titleBottom = layout.chrome.subtitle!.y + layout.chrome.subtitle!.style.fontSize;
+ expect(layout.annotations).toHaveLength(4);
+ const inside = layout.annotations.filter((a) => a.label!.bounds!.y >= layout.area.y);
+ expect(inside.length).toBeGreaterThan(0);
+ for (const annotation of layout.annotations)
+ expect(annotation.label!.bounds!.y).toBeGreaterThan(titleBottom);
+ expect(warnings.filter((m) => m.includes('exceed their available lane'))).toHaveLength(1);
+ });
+});
diff --git a/packages/engine/src/annotations/__tests__/resolve-text.test.ts b/packages/engine/src/annotations/__tests__/resolve-text.test.ts
index 9b3a3214..3d31719c 100644
--- a/packages/engine/src/annotations/__tests__/resolve-text.test.ts
+++ b/packages/engine/src/annotations/__tests__/resolve-text.test.ts
@@ -20,7 +20,6 @@ import {
DEFAULT_DOT_STROKE_WIDTH,
DEFAULT_LINE_HEIGHT,
FALLBACK_FONT_FAMILY,
- LEDE_FONT_WEIGHT,
LIGHT_CONNECTOR_STROKE,
LIGHT_DOT_FILL,
LIGHT_LABEL_BACKGROUND,
@@ -148,7 +147,13 @@ describe('text annotation: dot', () => {
// the data point.
it('does not resolve a default dot when the connector has an arrowhead', () => {
const spec = makeSpec([
- { type: 'text', x: '2020-01-01', y: 20, text: 'Arrowed', connector: 'curve' },
+ {
+ type: 'text',
+ x: '2020-01-01',
+ y: 20,
+ text: 'Arrowed',
+ connector: { type: 'curve', arrow: true },
+ },
]);
const scales = computeScales(spec, chartArea, spec.data);
const annotations = computeAnnotations(spec, {
@@ -785,7 +790,7 @@ describe('text annotation: connector suppression', () => {
});
describe('text annotation: connector config', () => {
- it('bare "curve" string defaults arrow to true', () => {
+ it('bare "curve" string defaults arrow to false', () => {
const spec = makeSpec([
{
type: 'text',
@@ -809,7 +814,7 @@ describe('text annotation: connector config', () => {
const c = annotations[0].label!.connector!;
expect(c.style).toBe('curve');
- expect(c.arrow).toBe(true);
+ expect(c.arrow).toBe(false);
});
it('bare "straight" string defaults arrow to false', () => {
@@ -920,7 +925,7 @@ describe('text annotation: connector config', () => {
expect(c.arrow).toBe(false);
});
- it('object form { type: "curve" } defaults arrow to true', () => {
+ it('object form { type: "curve" } defaults arrow to false', () => {
const spec = makeSpec([
{
type: 'text',
@@ -944,7 +949,7 @@ describe('text annotation: connector config', () => {
const c = annotations[0].label!.connector!;
expect(c.style).toBe('curve');
- expect(c.arrow).toBe(true);
+ expect(c.arrow).toBe(false);
});
it('connector: false disables connector entirely', () => {
@@ -1032,7 +1037,7 @@ describe('text annotation: typography', () => {
});
// Lede rule: a subtitle turns the primary line into a lede, so it goes bold.
- it('promotes the primary text to the lede weight when a subtitle is present', () => {
+ it('keeps primary text regular when a subtitle is present', () => {
const spec = makeSpec([
{ type: 'text', x: '2020-01-01', y: 20, text: 'Feb. 25', subtitle: '2015 maximum' },
]);
@@ -1047,7 +1052,7 @@ describe('text annotation: typography', () => {
measure: annotationMeasureForTest,
});
- expect(annotations[0].label!.style.fontWeight).toBe(LEDE_FONT_WEIGHT);
+ expect(annotations[0].label!.style.fontWeight).toBe(400);
// The subtitle never inherits it.
expect(annotations[0].subtitle!.style.fontWeight).toBe(SUBTITLE_FONT_WEIGHT);
});
diff --git a/packages/engine/src/annotations/__tests__/thinning.test.ts b/packages/engine/src/annotations/__tests__/thinning.test.ts
index b6118cae..418d4bd3 100644
--- a/packages/engine/src/annotations/__tests__/thinning.test.ts
+++ b/packages/engine/src/annotations/__tests__/thinning.test.ts
@@ -34,14 +34,14 @@ function makeSpec(text: string, overrides?: Partial): TextAnnota
}
describe('thinAnnotations', () => {
- it('preserves a demoted subtitle on the same note line', () => {
+ it('preserves primary text and subtitle separately in notes', () => {
const annotations = [makeResolved(100, 100, 'First'), makeResolved(105, 100, 'Second')];
const result = thinAnnotations(
annotations,
[makeSpec('First'), makeSpec('Second', { subtitle: 'Details' })],
measure,
);
- expect(result.footnotes).toEqual([{ index: 1, text: 'Second. Details' }]);
+ expect(result.footnotes).toEqual([{ index: 2, text: 'Second', subtitle: 'Details' }]);
});
it('keeps source identity on notes after filtering', () => {
const annotations = [
@@ -79,8 +79,8 @@ describe('thinAnnotations', () => {
const specs: Annotation[] = [makeSpec('First label'), makeSpec('Second label')];
const result = thinAnnotations(annotations, specs, measure);
expect(result.footnotes).toHaveLength(1);
- expect(result.footnotes[0]).toEqual({ index: 1, text: 'Second label' });
- expect(result.annotations[1].footnoteIndex).toBe(1);
+ expect(result.footnotes[0]).toEqual({ index: 2, text: 'Second label' });
+ expect(result.annotations[1].footnoteIndex).toBe(2);
expect(result.annotations[0].footnoteIndex).toBeUndefined();
});
@@ -122,7 +122,7 @@ describe('thinAnnotations', () => {
expect(result.footnotes).toHaveLength(0);
});
- it('assigns sequential footnote indices', () => {
+ it('uses source ordinals with gaps for inline annotations', () => {
const annotations = [
makeResolved(100, 100, 'Keep this'),
makeResolved(105, 100, 'Demote 1'),
@@ -131,8 +131,8 @@ describe('thinAnnotations', () => {
const specs: Annotation[] = [makeSpec('Keep this'), makeSpec('Demote 1'), makeSpec('Demote 2')];
const result = thinAnnotations(annotations, specs, measure);
expect(result.footnotes).toHaveLength(2);
- expect(result.footnotes[0].index).toBe(1);
- expect(result.footnotes[1].index).toBe(2);
+ expect(result.footnotes[0].index).toBe(2);
+ expect(result.footnotes[1].index).toBe(3);
});
it('maintains correct index alignment with mixed annotation types', () => {
@@ -153,7 +153,7 @@ describe('thinAnnotations', () => {
expect(result.footnotes).toHaveLength(1);
expect(result.footnotes[0].text).toBe('Second text');
expect(result.annotations[1].footnoteIndex).toBeUndefined();
- expect(result.annotations[2].footnoteIndex).toBe(1);
+ expect(result.annotations[2].footnoteIndex).toBe(2);
});
describe('plot-area containment', () => {
@@ -174,7 +174,7 @@ describe('thinAnnotations', () => {
expect(result.footnotes).toHaveLength(1);
expect(result.footnotes[0].text).toBe('Way too long to fit here');
expect(result.annotations[0].footnoteIndex).toBeUndefined();
- expect(result.annotations[1].footnoteIndex).toBe(1);
+ expect(result.annotations[1].footnoteIndex).toBe(2);
});
it('keeps a label that fits inside the plot', () => {
@@ -244,7 +244,7 @@ describe('thinAnnotations', () => {
const result = thinAnnotations(annotations, specs, measure, insetPlot, chart);
expect(result.footnotes).toHaveLength(1);
- expect(result.annotations[1].footnoteIndex).toBe(1);
+ expect(result.annotations[1].footnoteIndex).toBe(2);
});
});
diff --git a/packages/engine/src/annotations/__tests__/v9-defaults.test.ts b/packages/engine/src/annotations/__tests__/v9-defaults.test.ts
new file mode 100644
index 00000000..557d740b
--- /dev/null
+++ b/packages/engine/src/annotations/__tests__/v9-defaults.test.ts
@@ -0,0 +1,216 @@
+import type { ChartSpec, TextAnnotation } from '@opendata-ai/openchart-core';
+import { describe, expect, it } from 'vitest';
+import { compileChart } from '../../compile';
+import { migrateSpecV8 } from '../../migration/v8';
+
+const base: ChartSpec = {
+ mark: 'point',
+ data: [
+ { x: 0, y: 0 },
+ { x: 10, y: 10 },
+ ],
+ encoding: { x: { field: 'x', type: 'quantitative' }, y: { field: 'y', type: 'quantitative' } },
+ chrome: { title: 'A figure', source: 'Source' },
+ animation: false,
+};
+const text: TextAnnotation = {
+ type: 'text',
+ x: 5,
+ y: 5,
+ text: 'A long **annotation phrase** that needs to wrap across several lines',
+ subtitle: 'Supporting context',
+ connector: 'curve',
+ responsive: false,
+ anchor: 'left',
+ offset: { dx: -30, dy: -90 },
+};
+
+describe('v9 annotation defaults', () => {
+ it('places automatic text around the measured label of a range band', () => {
+ for (const y of [8, 9, 10]) {
+ const layout = compileChart(
+ {
+ ...base,
+ annotations: [
+ { type: 'range', x1: 2, x2: 8, label: 'A labeled interval near the peak' },
+ { type: 'text', x: 5, y, text: 'Peak value', subtitle: 'Supporting context' },
+ ],
+ },
+ { width: 800, height: 500 },
+ );
+ const range = layout.annotations.find((a) => a.type === 'range')!;
+ const text = layout.annotations.find((a) => a.type === 'text')!;
+ expect(range.bounds).toBeDefined();
+ const a = range.bounds!;
+ const b = text.bounds!;
+ expect(
+ Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x) > 1 &&
+ Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y) > 1,
+ ).toBe(false);
+ }
+ });
+ it('uses regular text, a 12/13 subtitle, bounded wrapping, and a quiet curve with a marker', () => {
+ const a = compileChart({ ...base, annotations: [text] }, { width: 800, height: 600 })
+ .annotations[0];
+ expect(a.label!.style.fontWeight).toBe(400);
+ expect(a.subtitle!.style.fontSize).toBe(12);
+ expect(a.label!.lines!.length).toBeGreaterThan(1);
+ expect(a.bounds!.width).toBeLessThanOrEqual(240);
+ expect(a.label!.connector!.arrow).toBe(false);
+ expect(a.dot).toBeDefined();
+ expect(a.label!.text).toBe(text.text);
+ });
+ it('materialized v8 styling keeps the former text, arrow, and wrapping choices', () => {
+ const migrated = migrateSpecV8({ ...base, annotations: [text] }).spec as ChartSpec;
+ const a = compileChart(migrated, { width: 800, height: 600 }).annotations[0];
+ expect(a.label!.style.fontWeight).toBe(700);
+ expect(a.subtitle!.style.fontSize).toBe(11);
+ expect(a.label!.lines).toBeUndefined();
+ expect(a.label!.connector!.arrow).toBe(true);
+ });
+ it('retains source text ordinals through filtering, priority and facet duplication', () => {
+ const annotations: ChartSpec['annotations'] = [
+ { type: 'text', x: 'missing', y: 100, text: 'Unresolved position' },
+ { type: 'refline', y: 4, label: 'Rule' },
+ { type: 'text', x: 3, y: 3, text: '**Second**', subtitle: '**Context**', priority: 20 },
+ { type: 'text', x: 7, y: 7, text: 'Third', priority: 1 },
+ ];
+ for (const facet of [false, true]) {
+ const spec: ChartSpec = {
+ ...base,
+ annotations,
+ responsive: { compactAnnotationLimit: 0 },
+ ...(facet
+ ? {
+ data: ['A', 'B'].flatMap((group) => base.data.map((row) => ({ ...row, group }))),
+ encoding: {
+ ...base.encoding,
+ facet: { field: 'group', type: 'nominal', columns: 2 },
+ },
+ }
+ : {}),
+ };
+ const layout = compileChart(spec, { width: 390, height: 700 });
+ expect(layout.chrome.footnotes!.map((n) => n.index)).toEqual([2, 3]);
+ expect(layout.chrome.footnotes![0]).toMatchObject({
+ text: '**Second**',
+ subtitle: '**Context**',
+ });
+ expect(layout.chrome.footnotes![0].spans!.flat().some((s) => s.bold)).toBe(true);
+ }
+ });
+ it.each([false, true])('grows only the note band by default (facet=%s)', (facet) => {
+ const spec: ChartSpec = {
+ ...base,
+ annotations: [
+ { ...text, responsive: undefined },
+ { ...text, x: 7 },
+ ],
+ ...(facet
+ ? {
+ data: ['A', 'B'].flatMap((group) => base.data.map((row) => ({ ...row, group }))),
+ encoding: { ...base.encoding, facet: { field: 'group', type: 'nominal', columns: 2 } },
+ }
+ : {}),
+ };
+ const options = { width: 800, height: 800 };
+ const current = compileChart(spec, options);
+ const subtract = compileChart({ ...spec, chromeLayout: 'subtract' }, options);
+ expect(current.chrome.grownHeight).toBeCloseTo(current.chrome.noteBandHeight!);
+ expect(current.dimensions.height).toBe(options.height + current.chrome.noteBandHeight!);
+ expect(current.area.height - subtract.area.height).toBeCloseTo(current.chrome.noteBandHeight!);
+ });
+});
+
+it('keeps auto-placed subtitle blocks clear of their own point markers', () => {
+ const layout = compileChart(
+ {
+ ...base,
+ annotations: [
+ {
+ type: 'text',
+ x: 5,
+ y: 8,
+ text: 'Callout',
+ subtitle: 'Context for the observation',
+ maxWidth: 130,
+ connector: 'curve',
+ },
+ ],
+ },
+ { width: 1280, height: 850 },
+ );
+ const a = layout.annotations[0];
+ const point = a.label!.connector?.endpoint ?? a.dot!;
+ const b = a.bounds!;
+ expect(
+ point.x >= b.x && point.x <= b.x + b.width && point.y >= b.y && point.y <= b.y + b.height,
+ ).toBe(false);
+});
+
+describe('compact override annotations', () => {
+ const twoCallouts: TextAnnotation[] = [
+ { id: 'p', type: 'text', x: 1, y: 1, text: 'A' },
+ { id: 'q', type: 'text', x: 9, y: 9, text: 'B' },
+ ];
+ it('renders author-written compact annotations inline without the default limit', () => {
+ const layout = compileChart(
+ { ...base, overrides: { compact: { annotations: twoCallouts } } } as ChartSpec,
+ { width: 360, height: 400, onWarn: () => {} },
+ );
+ expect(layout.annotations.map((a) => [a.id, a.footnoteIndex])).toEqual([
+ ['p', undefined],
+ ['q', undefined],
+ ]);
+ expect(layout.chrome.footnotes ?? []).toEqual([]);
+ });
+ it('still applies an explicit compactAnnotationLimit to compact override annotations', () => {
+ const layout = compileChart(
+ {
+ ...base,
+ responsive: { compactAnnotationLimit: 1 },
+ overrides: { compact: { annotations: twoCallouts } },
+ } as ChartSpec,
+ { width: 360, height: 400, onWarn: () => {} },
+ );
+ expect(layout.annotations.filter((a) => a.footnoteIndex !== undefined)).toHaveLength(1);
+ });
+});
+
+it('draws the leader that auto placement scored, even when the initial stub was too short', () => {
+ const layout = compileChart(
+ {
+ mark: 'point',
+ data: Array.from({ length: 11 }, (_, i) => ({ x: i * 10, y: i * 10 })),
+ encoding: {
+ x: { field: 'x', type: 'quantitative' },
+ y: { field: 'y', type: 'quantitative' },
+ },
+ // The shifted endpoint sits inside the initially resolved label box, so
+ // the resolver drops the stub; the placed label ends up well away from it.
+ annotations: [
+ {
+ type: 'text',
+ x: 50,
+ y: 50,
+ text: 'Callout text',
+ connectorOffset: { to: { dx: 20, dy: -20 } },
+ },
+ ],
+ } as ChartSpec,
+ { width: 600, height: 400, onWarn: () => {} },
+ );
+ const label = layout.annotations[0].label!;
+ const box = label.bounds!;
+ const point = layout.marks.find(
+ (m) => m.type === 'point' && (m.data as { x: number }).x === 50,
+ ) as { cx: number; cy: number };
+ const ex = point.cx + 20;
+ const ey = point.cy - 20;
+ const gap = Math.hypot(
+ Math.max(box.x - ex, 0, ex - box.x - box.width),
+ Math.max(box.y - ey, 0, ey - box.y - box.height),
+ );
+ expect(gap).toBeGreaterThan(20);
+ expect(label.connector).toBeDefined();
+});
diff --git a/packages/engine/src/annotations/collisions.ts b/packages/engine/src/annotations/collisions.ts
index 801492a2..62a7c54d 100644
--- a/packages/engine/src/annotations/collisions.ts
+++ b/packages/engine/src/annotations/collisions.ts
@@ -14,8 +14,9 @@ import type {
import { detectCollision, overlapArea } from '@opendata-ai/openchart-core';
import type { NormalizedChartSpec } from '../compiler/types';
import type { ResolvedScales } from '../layout/scales';
+import { ConnectorCrossings } from './connector-crossings';
import { CLAMP_MARGIN, DEFAULT_ANNOTATION_FONT_SIZE, NUDGE_PADDING } from './constants';
-import { type AnnotationMeasureTextFn, heuristicMeasure } from './geometry';
+import { type AnnotationMeasureTextFn, heuristicMeasure, stopAtTargetLabel } from './geometry';
import { annotationBlockBounds, moveAnnotationBy } from './move';
import { isAutoPlacement, type ObstacleKind, type PlacementObstacle } from './placement';
import { resolvePosition } from './position';
@@ -117,6 +118,15 @@ export function nudgeAnnotationFromObstacles(
const dropLineEndpointY =
label.connector?.style === 'drop-line' ? label.connector.endpoint?.y : undefined;
+ // Prefer a spot whose leader stays off the value labels; the first clear box
+ // is still the fallback when every leader crosses one.
+ const dataLabels = obstacles.filter(
+ (obs): obs is PlacementObstacle => 'kind' in obs && obs.kind === 'data-label',
+ );
+ const crossings = new ConnectorCrossings();
+ for (const box of dataLabels) crossings.addBox(box);
+ let fallback: { dx: number; dy: number } | undefined;
+
for (const { dx, dy } of candidates) {
const candidateBounds: Rect = { ...bounds, x: bounds.x + dx, y: bounds.y + dy };
@@ -147,12 +157,25 @@ export function nudgeAnnotationFromObstacles(
labelCenterY >= chartArea.y - fontSize &&
labelCenterY <= chartArea.y + chartArea.height + fontSize * 3;
- if (inBounds) {
- moveAnnotationBy(annotation, label, dx, dy, measure);
- return true;
+ if (!inBounds) continue;
+ if (label.connector && dataLabels.length > 0) {
+ // The mover replaces label, subtitle and bounds, so a shallow copy is a trial.
+ const trial = { ...annotation };
+ moveAnnotationBy(trial, label, dx, dy, measure);
+ const connector = trial.label?.connector;
+ if (crossings.score((connector && stopAtTargetLabel(connector, dataLabels)) || connector)) {
+ fallback ??= { dx, dy };
+ continue;
+ }
}
+ moveAnnotationBy(annotation, label, dx, dy, measure);
+ return true;
}
+ if (fallback) {
+ moveAnnotationBy(annotation, label, fallback.dx, fallback.dy, measure);
+ return true;
+ }
return false;
}
diff --git a/packages/engine/src/annotations/compute.ts b/packages/engine/src/annotations/compute.ts
index 658e7882..ccf8b046 100644
--- a/packages/engine/src/annotations/compute.ts
+++ b/packages/engine/src/annotations/compute.ts
@@ -1,9 +1,11 @@
+import { ConnectorCrossings } from './connector-crossings';
import { rememberAnnotationGeometry } from './edit-context';
import {
+ defaultReferenceContainer,
+ NO_REFERENCE_RESERVE,
placeReferenceLabel,
type ReferenceLanes,
type ReferenceReserve,
- referenceLabelReserve,
} from './reference-layout';
/**
* Annotation computation: converts spec-level annotations to pixel-positioned
@@ -32,9 +34,9 @@ import {
nudgeRefLineLabel,
resolveAnnotationCollisions,
} from './collisions';
-import { SUBTITLE_FONT_WEIGHT, subtitleFontSize } from './constants';
-import type { AnnotationMeasureTextFn } from './geometry';
-import { moveAnnotationTo } from './move';
+import { DEFAULT_ANNOTATION_FONT_SIZE, SUBTITLE_FONT_WEIGHT, subtitleFontSize } from './constants';
+import { type AnnotationMeasureTextFn, stopAtTargetLabel } from './geometry';
+import { annotationBlockBounds, moveAnnotationTo } from './move';
import { findBestPlacement, isAutoPlacement, type PlacementObstacle } from './placement';
import { resolvePosition } from './position';
import { resolveRangeAnnotation } from './resolve-range';
@@ -43,6 +45,7 @@ import {
makeAnnotationLabelStyle,
resolveLedeFontWeight,
resolveTextAnnotation,
+ textConnectorSeed,
} from './resolve-text';
export interface AnnotationContext {
@@ -50,6 +53,10 @@ export interface AnnotationContext {
chartArea: Rect;
referenceContainer?: Rect;
referenceLanes?: ReferenceLanes;
+ /**
+ * Outside reference lanes resolved by the figure budget, the only source of
+ * the reserve. Without one, outside labels place inside the plot.
+ */
referenceReserve?: ReferenceReserve;
strategy: LayoutStrategy;
isDark: boolean;
@@ -58,6 +65,8 @@ export interface AnnotationContext {
measure: AnnotationMeasureTextFn;
/** Theme font stack, so annotation text is measured and rendered in the same face. */
fontFamily?: string;
+ padding?: number;
+ smallFontSize?: number;
colors?: import('@opendata-ai/openchart-core').ThemeColors;
debugPlacement?: boolean;
onWarn?: (message: string) => void;
@@ -69,6 +78,17 @@ export interface AnnotationContext {
* the title/subtitle. Defaults to the plain SVG clamp margin.
*/
topBound?: number;
+ /** Endpoint markers drawn on data points (see `adoptPointMarker`). */
+ pointMarkers?: ReadonlyArray<{ x: number; y: number; fill: string; stroke: string }>;
+}
+
+/**
+ * Default callout wrap width: up to 240px inside the padded plot, but never
+ * narrower than about four ems, so a tiny plot does not stack one glyph per line.
+ */
+function defaultCalloutMaxWidth(annotation: TextAnnotation, ctx: AnnotationContext): number {
+ const floor = (annotation.fontSize ?? DEFAULT_ANNOTATION_FONT_SIZE) * 4;
+ return Math.max(floor, Math.min(240, ctx.chartArea.width - 2 * (ctx.padding ?? 16)));
}
/**
@@ -84,6 +104,20 @@ export interface AnnotationContext {
* annotation collisions are resolved using a greedy placement algorithm.
* Finally, labels are clamped to stay within the total SVG bounds.
*/
+/**
+ * A default annotation dot on a point that already carries an endpoint marker
+ * takes the marker's colors, so the two draw as one ring instead of two.
+ */
+function adoptPointMarker(
+ dot: NonNullable,
+ markers: AnnotationContext['pointMarkers'],
+): void {
+ const marker = markers?.find((m) => Math.abs(m.x - dot.x) < 0.5 && Math.abs(m.y - dot.y) < 0.5);
+ if (!marker) return;
+ dot.fill = marker.fill;
+ dot.stroke = marker.stroke;
+}
+
export function computeAnnotations(
spec: NormalizedChartSpec,
ctx: AnnotationContext,
@@ -92,9 +126,10 @@ export function computeAnnotations(
const svgRect: Rect = { x: 0, y: 0, width: ctx.svg.width, height: ctx.svg.height };
const annotations: ResolvedAnnotation[] = [];
- const referenceReserve =
- ctx.referenceReserve ?? referenceLabelReserve(spec.annotations, ctx.svg.width, ctx.measure);
+ const referenceReserve = ctx.referenceReserve ?? NO_REFERENCE_RESERVE;
const referenceLanes: ReferenceLanes = ctx.referenceLanes ?? {};
+ // Value labels a leader may run into on its way to its own data point.
+ const dataLabels = ctx.obstacles.filter((obstacle) => obstacle.kind === 'data-label');
// Deferred auto-placement entries (resolved in Pass 2)
const autoQueue: Array<{
@@ -106,7 +141,17 @@ export function computeAnnotations(
}> = [];
// ---- Pass 1: resolve explicit annotations and queue auto ones ----
- for (const [specIndex, annotation] of spec.annotations.entries()) {
+ for (const [specIndex, sourceAnnotation] of spec.annotations.entries()) {
+ const annotation =
+ sourceAnnotation.type === 'text'
+ ? {
+ ...sourceAnnotation,
+ maxWidth:
+ sourceAnnotation.maxWidth === undefined
+ ? defaultCalloutMaxWidth(sourceAnnotation, ctx)
+ : sourceAnnotation.maxWidth,
+ }
+ : { ...sourceAnnotation, fontSize: sourceAnnotation.fontSize ?? ctx.smallFontSize ?? 12 };
if (isCompact && annotation.responsive !== false) {
continue;
}
@@ -165,6 +210,10 @@ export function computeAnnotations(
}
if (resolved) {
+ // Range labels are fixed obstacles too; their bands are not. Record the
+ // measured text block before automatic callouts choose a placement.
+ if (resolved.label && !resolved.bounds)
+ resolved.bounds = annotationBlockBounds(resolved, resolved.label, ctx.measure);
if (annotation.type === 'text' && ctx.obstacles.length > 0) {
nudgeAnnotationFromObstacles(
resolved,
@@ -175,17 +224,16 @@ export function computeAnnotations(
ctx.measure,
);
}
+ // The resolved array is filtered (an out-of-domain annotation resolves to
+ // nothing), so carry the spec index along -- it's the only stable way back
+ // to the authored annotation from here on (reference lanes key on it too).
+ resolved.specIndex = specIndex;
if (annotation.type === 'refline' && annotation.labelPlacement)
placeReferenceLabel(
annotation,
resolved,
ctx.chartArea,
- ctx.referenceContainer ?? {
- x: 8,
- y: ctx.topBound ?? 8,
- width: ctx.svg.width - 16,
- height: ctx.svg.height - (ctx.topBound ?? 8) - 8,
- },
+ ctx.referenceContainer ?? defaultReferenceContainer(ctx.svg, ctx.topBound),
referenceReserve,
referenceLanes,
ctx.measure,
@@ -201,10 +249,6 @@ export function computeAnnotations(
if (annotation.type === 'refline' && !annotation.labelPlacement && ctx.obstacles.length > 0) {
nudgeRefLineLabel(resolved, annotation, ctx.obstacles, ctx.chartArea, ctx.measure);
}
- // The resolved array is filtered (an out-of-domain annotation resolves to
- // nothing), so carry the spec index along -- it's the only stable way back
- // to the authored annotation from here on.
- resolved.specIndex = specIndex;
annotations.push(resolved);
}
}
@@ -215,14 +259,34 @@ export function computeAnnotations(
const workingObstacles: PlacementObstacle[] = [...ctx.obstacles];
for (let i = 0; i < annotations.length; i++) {
const a = annotations[i];
- if (a.bounds && !autoQueue.some((q) => q.index === i)) {
+ if (a.bounds && !autoQueue.some((q) => q.resolved === a)) {
workingObstacles.push({ ...a.bounds, kind: 'annotation' } as PlacementObstacle);
}
}
+ const crossingIndex = new ConnectorCrossings();
+ for (const obstacle of workingObstacles) crossingIndex.addBox(obstacle);
+ for (const placed of annotations) {
+ if (!autoQueue.some((entry) => entry.resolved === placed))
+ crossingIndex.addConnector(placed.label?.connector);
+ }
+ autoQueue.sort(
+ (a, b) =>
+ (a.annotation.priority ?? Number.MAX_SAFE_INTEGER) -
+ (b.annotation.priority ?? Number.MAX_SAFE_INTEGER) || a.index - b.index,
+ );
for (const entry of autoQueue) {
const { annotation, anchorX, anchorY, resolved } = entry;
- // Must mirror resolveTextAnnotation exactly (lede weight, theme font):
+ // Seed before the search so the leader it scores is the one the commit
+ // below rebuilds; a too-short initial stub left the label unconnected.
+ if (resolved.label)
+ resolved.label.connector ??= textConnectorSeed(
+ annotation,
+ ctx.scales,
+ ctx.isDark,
+ ctx.colors,
+ );
+ // Must mirror resolveTextAnnotation exactly (font weight, theme font):
// findBestPlacement measures with this style, and the resolved annotation
// renders with the other one. Any drift and the scored bounds are a lie.
const labelStyle = makeAnnotationLabelStyle(
@@ -259,6 +323,20 @@ export function computeAnnotations(
svgRect,
ctx.measure,
ctx.debugPlacement,
+ (x, y, anchor) => {
+ // No connector config means no leader at any candidate.
+ if (!resolved.label?.connector) return 0;
+ // The mover replaces label, subtitle and bounds rather than mutating
+ // them, so a shallow copy keeps the resolved annotation untouched.
+ const candidate = { ...resolved };
+ moveAnnotationTo(candidate, resolved.label, x, y, ctx.measure, anchor);
+ const connector = candidate.label?.connector;
+ // Score the leader as it will draw: stopped short of its own value
+ // label. One cut to nothing still strikes that label, so it scores whole.
+ return crossingIndex.score(
+ (connector && stopAtTargetLabel(connector, dataLabels)) || connector,
+ );
+ },
);
// Commit the placement. `moveAnnotationTo` owns every piece of the block's
@@ -284,6 +362,8 @@ export function computeAnnotations(
// search's own estimate of it. One of them has to be the source of truth.
const placedBounds = resolved.bounds ?? result.bounds;
workingObstacles.push({ ...placedBounds, kind: 'annotation' } as PlacementObstacle);
+ crossingIndex.addBox({ ...placedBounds, kind: 'annotation' });
+ crossingIndex.addConnector(resolved.label?.connector);
}
}
@@ -324,7 +404,28 @@ export function computeAnnotations(
for (const resolved of annotations) {
const authored = spec.annotations[resolved.specIndex!];
- if (authored?.type === 'text') rememberAnnotationGeometry(resolved, authored, ctx);
+ // Routing, not placement: an authored block stays put while its leader
+ // stops short of the value label it would otherwise strike through.
+ const connector = resolved.label?.connector;
+ if (authored?.type === 'text' && connector && dataLabels.length > 0) {
+ const stopped = stopAtTargetLabel(connector, dataLabels);
+ if (stopped !== undefined)
+ resolved.label = { ...resolved.label!, connector: stopped ?? undefined };
+ }
+ if (authored?.type === 'text' && authored.dot === undefined && resolved.dot)
+ adoptPointMarker(resolved.dot, ctx.pointMarkers);
+ if (authored?.type === 'text')
+ rememberAnnotationGeometry(
+ resolved,
+ {
+ ...authored,
+ maxWidth:
+ authored.maxWidth === undefined
+ ? defaultCalloutMaxWidth(authored, ctx)
+ : authored.maxWidth,
+ },
+ ctx,
+ );
}
return annotations;
}
diff --git a/packages/engine/src/annotations/connector-crossings.ts b/packages/engine/src/annotations/connector-crossings.ts
new file mode 100644
index 00000000..229ef0fa
--- /dev/null
+++ b/packages/engine/src/annotations/connector-crossings.ts
@@ -0,0 +1,160 @@
+import type { Point, Rect, ResolvedLabel } from '@opendata-ai/openchart-core';
+import type { PlacementObstacle } from './placement';
+
+type Segment = { a: Point; b: Point };
+type Obstacle = {
+ bounds: Rect;
+ segment?: Segment;
+ kind: PlacementObstacle['kind'];
+ /** Stamps for the last segment that tested this obstacle and the last score it counted toward. */
+ tested: number;
+ counted: number;
+};
+const CELL = 64;
+const contains = (r: Rect, p: Point) =>
+ p.x >= r.x && p.x <= r.x + r.width && p.y >= r.y && p.y <= r.y + r.height;
+const overlaps = (a: Rect, b: Rect) =>
+ a.x <= b.x + b.width && b.x <= a.x + a.width && a.y <= b.y + b.height && b.y <= a.y + a.height;
+const bounds = ({ a, b }: Segment): Rect => ({
+ x: Math.min(a.x, b.x),
+ y: Math.min(a.y, b.y),
+ width: Math.abs(a.x - b.x),
+ height: Math.abs(a.y - b.y),
+});
+
+function intersects(a: Segment, b: Segment): boolean {
+ const cross = (p: Point, q: Point, r: Point) =>
+ (q.x - p.x) * (r.y - p.y) - (q.y - p.y) * (r.x - p.x);
+ const c1 = cross(a.a, a.b, b.a),
+ c2 = cross(a.a, a.b, b.b);
+ const c3 = cross(b.a, b.b, a.a),
+ c4 = cross(b.a, b.b, a.b);
+ if (c1 * c2 > 0 || c3 * c4 > 0) return false;
+ const ra = bounds(a),
+ rb = bounds(b);
+ return (
+ ra.x <= rb.x + rb.width &&
+ rb.x <= ra.x + ra.width &&
+ ra.y <= rb.y + rb.height &&
+ rb.y <= ra.y + ra.height
+ );
+}
+function crossesRect(s: Segment, r: Rect): boolean {
+ if (contains(r, s.a) || contains(r, s.b)) return true;
+ const points = [
+ { x: r.x, y: r.y },
+ { x: r.x + r.width, y: r.y },
+ { x: r.x + r.width, y: r.y + r.height },
+ { x: r.x, y: r.y + r.height },
+ ];
+ return points.some((a, i) => intersects(s, { a, b: points[(i + 1) % 4] }));
+}
+/** Bounded curve sampling uses the same resolved control point as rendering. */
+function segments(connector: NonNullable): Segment[] {
+ if (connector.style !== 'curve' || !connector.control)
+ return [{ a: connector.from, b: connector.to }];
+ const result: Segment[] = [];
+ let previous = connector.from;
+ for (let i = 1; i <= 16; i++) {
+ const t = i / 16,
+ u = 1 - t;
+ const next = {
+ x: u * u * connector.from.x + 2 * u * t * connector.control.x + t * t * connector.to.x,
+ y: u * u * connector.from.y + 2 * u * t * connector.control.y + t * t * connector.to.y,
+ };
+ result.push({ a: previous, b: next });
+ previous = next;
+ }
+ return result;
+}
+
+/** Spatially indexed leader/label/mark crossings for bounded placement search. */
+export class ConnectorCrossings {
+ private cells = new Map>();
+ private all = new Set();
+ private oversized = new Set();
+ private stamp = 0;
+ private keys(r: Rect): string[] | undefined {
+ const count =
+ (Math.floor((r.x + r.width) / CELL) - Math.floor(r.x / CELL) + 1) *
+ (Math.floor((r.y + r.height) / CELL) - Math.floor(r.y / CELL) + 1);
+ if (!Number.isFinite(count) || count > 1024) return undefined;
+ const keys: string[] = [];
+ for (let x = Math.floor(r.x / CELL); x <= Math.floor((r.x + r.width) / CELL); x++)
+ for (let y = Math.floor(r.y / CELL); y <= Math.floor((r.y + r.height) / CELL); y++)
+ keys.push(`${x},${y}`);
+ return keys;
+ }
+ private insert(obstacle: Obstacle): void {
+ this.all.add(obstacle);
+ const keys = this.keys(obstacle.bounds);
+ if (!keys) {
+ this.oversized.add(obstacle);
+ return;
+ }
+ for (const key of keys) {
+ let cell = this.cells.get(key);
+ if (!cell) {
+ cell = new Set();
+ this.cells.set(key, cell);
+ }
+ cell.add(obstacle);
+ }
+ }
+ addBox(box: PlacementObstacle): void {
+ this.insert({ bounds: box, kind: box.kind, tested: 0, counted: 0 });
+ }
+ addConnector(connector: ResolvedLabel['connector']): void {
+ if (connector)
+ for (const segment of segments(connector))
+ this.insert({
+ bounds: bounds(segment),
+ segment,
+ kind: 'annotation',
+ tested: 0,
+ counted: 0,
+ });
+ }
+ score(connector: ResolvedLabel['connector']): number {
+ if (!connector) return 0;
+ // Stamps instead of per-segment Sets: this runs once per placement candidate.
+ const scoreStamp = ++this.stamp;
+ let hits = 0;
+ for (const segment of segments(connector)) {
+ const segmentStamp = ++this.stamp;
+ const test = (obstacle: Obstacle) => {
+ if (obstacle.tested === segmentStamp || obstacle.counted === scoreStamp) return;
+ obstacle.tested = segmentStamp;
+ // The observation being explained is not an unrelated obstacle.
+ if (
+ (obstacle.kind === 'mark' || obstacle.kind === 'line' || obstacle.kind === 'area-fill') &&
+ connector.endpoint &&
+ contains(obstacle.bounds, connector.endpoint)
+ )
+ return;
+ // Disjoint boxes can neither cross nor contain: skip the exact tests.
+ if (!overlaps(box, obstacle.bounds)) return;
+ if (
+ obstacle.segment
+ ? intersects(segment, obstacle.segment)
+ : crossesRect(segment, obstacle.bounds)
+ ) {
+ obstacle.counted = scoreStamp;
+ hits++;
+ }
+ };
+ const box = bounds(segment);
+ const keys = this.keys(box);
+ if (!keys) {
+ for (const obstacle of this.all) test(obstacle);
+ continue;
+ }
+ for (const obstacle of this.oversized) test(obstacle);
+ for (const key of keys) {
+ const cell = this.cells.get(key);
+ if (cell) for (const obstacle of cell) test(obstacle);
+ }
+ }
+ return hits * 2;
+ }
+}
diff --git a/packages/engine/src/annotations/constants.ts b/packages/engine/src/annotations/constants.ts
index 6f766639..bba61add 100644
--- a/packages/engine/src/annotations/constants.ts
+++ b/packages/engine/src/annotations/constants.ts
@@ -7,13 +7,7 @@ export const DEFAULT_ANNOTATION_FONT_SIZE = 13;
*/
export const DEFAULT_ANNOTATION_FONT_WEIGHT = 400;
-/**
- * Weight the primary text takes when a subtitle is present and the author set no
- * explicit `fontWeight` — the "Feb. 25 / 2015 maximum" lede + context stack.
- */
-export const LEDE_FONT_WEIGHT = 700;
-
-/** Subtitles are always regular, so they never inherit a lede weight. */
+/** Subtitles are always regular, whatever the primary text's weight. */
export const SUBTITLE_FONT_WEIGHT = 400;
/** Font stack used when the theme font isn't threaded through (SSR/legacy paths). */
@@ -67,7 +61,7 @@ export const DEFAULT_DOT_STROKE_WIDTH = 1.5;
export const SUBTITLE_GAP = 2;
/** Subtitle font size multiplier (relative to primary annotation font size). */
-export const SUBTITLE_FONT_SIZE_RATIO = 0.85;
+export const SUBTITLE_FONT_SIZE_RATIO = 12 / 13;
/**
* The subtitle's resolved font size. Rounded, and shared by every caller: the
@@ -104,6 +98,15 @@ export function subtitleBaselineY(
/** Gap (px) between the label bounding box and the start of its connector. */
export const CONNECTOR_STANDOFF = 6;
+/** Gap (px) between a connector's data end and the value label it stops at. */
+export const TARGET_LABEL_STANDOFF = 3;
+
+/**
+ * A value label whose box sits within this distance (px) of a connector's data
+ * point is that point's own label. Column value labels sit a few px above the bar.
+ */
+export const TARGET_LABEL_REACH = 16;
+
/**
* Connectors shorter than this are suppressed: a nub between a label and the
* marker it's already touching reads as noise, not as a leader.
diff --git a/packages/engine/src/annotations/geometry.ts b/packages/engine/src/annotations/geometry.ts
index c9089a29..40cd90df 100644
--- a/packages/engine/src/annotations/geometry.ts
+++ b/packages/engine/src/annotations/geometry.ts
@@ -19,6 +19,8 @@ import {
DEFAULT_ANNOTATION_FONT_WEIGHT,
DEFAULT_LINE_HEIGHT,
DROP_LINE_TOP_GAP,
+ TARGET_LABEL_REACH,
+ TARGET_LABEL_STANDOFF,
} from './constants';
import { BOLD_SPAN_FONT_WEIGHT, parseAnnotationSpans } from './rich-text';
@@ -329,11 +331,160 @@ export function automaticControlPoint(
return bend === 0 ? { x, y } : { x: x + bend * dy, y: y - bend * dx };
}
+/**
+ * Pull a connector's data end back from `endpoint` by `gap`. A straight line
+ * shortens along its chord. A curve is shaped to the real endpoint and then
+ * shortened along its own end tangent (control -> endpoint), so the tail still
+ * aims at the marker; shortening along the chord bends the tail off it.
+ */
+export function pullBackConnector(
+ from: { x: number; y: number },
+ endpoint: { x: number; y: number },
+ gap: number,
+ curve?: { exit?: 'horizontal' | 'vertical'; bend?: number },
+): { to: { x: number; y: number }; control?: { x: number; y: number } } {
+ const control = curve ? automaticControlPoint(from, endpoint, curve.exit, curve.bend) : undefined;
+ const pullback = (origin: { x: number; y: number }) => {
+ const dx = endpoint.x - origin.x;
+ const dy = endpoint.y - origin.y;
+ const dist = Math.sqrt(dx * dx + dy * dy);
+ return dist > 0
+ ? { x: endpoint.x - (dx / dist) * gap, y: endpoint.y - (dy / dist) * gap }
+ : null;
+ };
+ const to = (control && pullback(control)) ?? pullback(from) ?? { ...endpoint };
+ return control ? { to, control } : { to };
+}
+
+type Connector = NonNullable;
+
+/** Point at parameter `t` along the drawn connector: its chord, or the rendered quadratic. */
+function connectorPointAt(connector: Connector, t: number): { x: number; y: number } {
+ const { from, to, control } = connector;
+ if (connector.style !== 'curve' || !control)
+ return { x: from.x + (to.x - from.x) * t, y: from.y + (to.y - from.y) * t };
+ const u = 1 - t;
+ return {
+ x: u * u * from.x + 2 * u * t * control.x + t * t * to.x,
+ y: u * u * from.y + 2 * u * t * control.y + t * t * to.y,
+ };
+}
+
+/** First parameter at which the drawn connector enters `box`; undefined when it never does. */
+function connectorEntry(connector: Connector, box: Rect): number | undefined {
+ const inside = (p: { x: number; y: number }) =>
+ p.x >= box.x && p.x <= box.x + box.width && p.y >= box.y && p.y <= box.y + box.height;
+ // A leader that starts inside the label has no clear stretch to keep.
+ if (inside(connector.from)) return undefined;
+ if (connector.style !== 'curve' || !connector.control) {
+ // Liang-Barsky clip of the chord against the box.
+ const { from, to } = connector;
+ const dx = to.x - from.x;
+ const dy = to.y - from.y;
+ let t0 = 0;
+ let t1 = 1;
+ const clip = (p: number, q: number) => {
+ if (p === 0) return q >= 0;
+ const r = q / p;
+ if (p < 0) {
+ if (r > t1) return false;
+ if (r > t0) t0 = r;
+ } else {
+ if (r < t0) return false;
+ if (r < t1) t1 = r;
+ }
+ return true;
+ };
+ return clip(-dx, from.x - box.x) &&
+ clip(dx, box.x + box.width - from.x) &&
+ clip(-dy, from.y - box.y) &&
+ clip(dy, box.y + box.height - from.y)
+ ? t0
+ : undefined;
+ }
+ // A quadratic: find the first sampled step inside, then bisect to the edge.
+ const steps = 64;
+ for (let i = 1; i <= steps; i++) {
+ if (!inside(connectorPointAt(connector, i / steps))) continue;
+ let lo = (i - 1) / steps;
+ let hi = i / steps;
+ for (let k = 0; k < 24; k++) {
+ const mid = (lo + hi) / 2;
+ if (inside(connectorPointAt(connector, mid))) hi = mid;
+ else lo = mid;
+ }
+ return hi;
+ }
+ return undefined;
+}
+
+/**
+ * Stop a connector's data end where it would strike through the target's own
+ * value label, a standoff short of the label box. Only labels within
+ * `TARGET_LABEL_REACH` of the data point count: a value label elsewhere on the
+ * leader's path is a placement concern, not a reason to cut the line short. A
+ * curve is split at the cut (de Casteljau), so the kept part keeps its shape.
+ *
+ * Returns `undefined` when no target label is crossed, `null` when the line
+ * left before the label is too short to draw, else the shortened connector
+ * carrying the label as `targetLabel` for later rebuilds.
+ */
+export function stopAtTargetLabel(
+ connector: Connector,
+ labels: readonly Rect[],
+): Connector | null | undefined {
+ const endpoint = connector.endpoint ?? connector.to;
+ let cut: { t: number; label: Rect } | undefined;
+ for (const label of labels) {
+ const gapX = Math.max(label.x - endpoint.x, 0, endpoint.x - (label.x + label.width));
+ const gapY = Math.max(label.y - endpoint.y, 0, endpoint.y - (label.y + label.height));
+ if (Math.hypot(gapX, gapY) > TARGET_LABEL_REACH) continue;
+ const t = connectorEntry(connector, {
+ x: label.x - TARGET_LABEL_STANDOFF,
+ y: label.y - TARGET_LABEL_STANDOFF,
+ width: label.width + 2 * TARGET_LABEL_STANDOFF,
+ height: label.height + 2 * TARGET_LABEL_STANDOFF,
+ });
+ if (t !== undefined && (!cut || t < cut.t)) cut = { t, label };
+ }
+ if (!cut) return undefined;
+ const to = connectorPointAt(connector, cut.t);
+ const { from, control } = connector;
+ if (!connectorIsDrawable(Math.hypot(to.x - from.x, to.y - from.y), connector.arrow)) return null;
+ const { x, y, width, height } = cut.label;
+ return {
+ ...connector,
+ to,
+ ...(connector.style === 'curve' && control
+ ? {
+ control: {
+ x: from.x + (control.x - from.x) * cut.t,
+ y: from.y + (control.y - from.y) * cut.t,
+ },
+ }
+ : {}),
+ targetLabel: { x, y, width, height },
+ };
+}
+
export function refreshConnector(
- connector: NonNullable,
+ connector: Connector,
bounds: Rect,
markerRadius: number,
keepVisible = false,
+): ResolvedLabel['connector'] {
+ const rebuilt = rebuildConnector(connector, bounds, markerRadius, keepVisible);
+ if (!rebuilt?.targetLabel) return rebuilt;
+ const stopped = stopAtTargetLabel(rebuilt, [rebuilt.targetLabel]);
+ if (stopped === null) return keepVisible ? rebuilt : undefined;
+ return stopped ?? rebuilt;
+}
+
+function rebuildConnector(
+ connector: Connector,
+ bounds: Rect,
+ markerRadius: number,
+ keepVisible: boolean,
): ResolvedLabel['connector'] {
// `endpoint` is the un-pulled-back data point, and the only valid basis for a
// rebuild: pulling back from `to` (already pulled back) shrinks the line a
@@ -391,14 +542,12 @@ export function refreshConnector(
y: exit.y + (connector.fromOffset?.dy ?? 0),
};
- const gap = connectorPullbackGap(markerRadius, connector.arrow);
- const cdx = endpoint.x - from.x;
- const cdy = endpoint.y - from.y;
- const dist = Math.sqrt(cdx * cdx + cdy * cdy);
- const to =
- dist > 0
- ? { x: endpoint.x - (cdx / dist) * gap, y: endpoint.y - (cdy / dist) * gap }
- : { ...endpoint };
+ const { to, control } = pullBackConnector(
+ from,
+ endpoint,
+ connectorPullbackGap(markerRadius, connector.arrow),
+ connector.style === 'curve' ? { exit: exit.exit, bend: connector.bend } : undefined,
+ );
// Min-length check runs AFTER the pullback: a near-degenerate line can flip
// direction once its end is pulled back, and checking first would miss it.
@@ -412,8 +561,6 @@ export function refreshConnector(
from,
to,
exit: exit.exit,
- ...(connector.style === 'curve'
- ? { control: automaticControlPoint(from, to, exit.exit, connector.bend) }
- : {}),
+ ...(control ? { control } : {}),
};
}
diff --git a/packages/engine/src/annotations/notes.ts b/packages/engine/src/annotations/notes.ts
index b3c9eaba..c08533b4 100644
--- a/packages/engine/src/annotations/notes.ts
+++ b/packages/engine/src/annotations/notes.ts
@@ -4,7 +4,9 @@ import type {
MeasureTextFn,
ResolvedTheme,
} from '@opendata-ai/openchart-core';
-import { estimateTextWidth, wrapText } from '@opendata-ai/openchart-core';
+import { estimateTextWidth } from '@opendata-ai/openchart-core';
+import { parseAnnotationSpans } from './rich-text';
+import { wrapAnnotationText } from './wrap-text';
/** Resolve note lines once, so figure sizing, SVG rendering and export agree. */
export function wrapAnnotationNotes(
@@ -16,24 +18,17 @@ export function wrapAnnotationNotes(
const size = theme.fonts.sizes.small;
const weight = theme.fonts.weights.normal;
const maxWidth = Math.max(1, width - 2 * theme.spacing.padding);
- const measure = (text: string) =>
- measureText?.(text, size, weight).width ?? estimateTextWidth(text, size, weight);
+ const measure = (text: string, style: { fontSize: number; fontWeight?: number | string }) =>
+ measureText?.(text, style.fontSize, Number(style.fontWeight) || weight).width ??
+ estimateTextWidth(text, style.fontSize, Number(style.fontWeight) || weight);
return notes.map((note) => {
- const text = `${note.index}. ${note.text}`;
- const lines = wrapText(text, size, weight, maxWidth, measureText).flatMap((line) => {
- // Word wrapping alone cannot contain an unbroken URL or identifier.
- const chunks: string[] = [];
- let chunk = '';
- for (const character of line) {
- if (chunk && measure(chunk + character) > maxWidth) {
- chunks.push(chunk);
- chunk = character;
- } else chunk += character;
- }
- chunks.push(chunk);
- return chunks;
- });
- return lines.length > 1 ? { ...note, lines } : note;
+ const lines = wrapAnnotationText(
+ `${note.index}. ${note.text}${note.subtitle ? `\n${note.subtitle}` : ''}`,
+ maxWidth,
+ { fontSize: size, fontWeight: weight },
+ measure,
+ );
+ return { ...note, lines, spans: lines.map(parseAnnotationSpans) };
});
}
@@ -55,15 +50,14 @@ export function annotationNoteCapacity(
? [
{
specIndex,
- text: annotation.subtitle
- ? `${annotation.text}. ${annotation.subtitle}`
- : annotation.text,
+ text: annotation.text,
+ subtitle: annotation.subtitle,
},
]
: [],
);
- // Priority and placement determine the eventual numbering. Budget every source
- // with the widest possible prefix, including proportional numeral fonts.
+ // Reserve the widest source ordinal so placement and drag changes cannot
+ // change capacity, including with proportional numeral fonts.
const size = theme.fonts.sizes.small;
const weight = theme.fonts.weights.normal;
const measure = (text: string) =>
diff --git a/packages/engine/src/annotations/placement.ts b/packages/engine/src/annotations/placement.ts
index d1105e0c..04c3ec0b 100644
--- a/packages/engine/src/annotations/placement.ts
+++ b/packages/engine/src/annotations/placement.ts
@@ -450,6 +450,7 @@ export function findBestPlacement(
svgRect: Rect,
measure: AnnotationMeasureTextFn = heuristicMeasure,
debug = false,
+ connectorScore?: (x: number, y: number, anchor: 'start' | 'end') => number,
): PlacementResult {
const isUpperHalf = anchorY < chartArea.y + chartArea.height / 2;
const dirOrder = isUpperHalf ? UPPER_HALF_ORDER : LOWER_HALF_ORDER;
@@ -466,7 +467,23 @@ export function findBestPlacement(
},
measure,
);
- const boxHeight = sampleBounds.height;
+ const subtitleSample =
+ subtitleText && subtitleStyle
+ ? computeTextBlockBounds(
+ 0,
+ subtitleBaselineY(
+ style.fontSize,
+ style.fontSize * style.lineHeight,
+ text.split('\n').length,
+ ),
+ subtitleText,
+ { ...subtitleStyle, textAnchor: 'start' },
+ measure,
+ )
+ : undefined;
+ const boxHeight = subtitleSample
+ ? unionRects(sampleBounds, subtitleSample).height
+ : sampleBounds.height;
// Centering a `*-center` attach needs the width of the WHOLE block, not just
// the primary line: a subtitle is often the wider of the two, and centering on
@@ -566,7 +583,7 @@ export function findBestPlacement(
box,
directionIndex: di,
ring: ring as 1 | 2 | 3,
- score,
+ score: score + (connectorScore?.(labelX, labelY, dir.textAnchor) ?? 0),
candidateIdx: idx,
});
}
@@ -603,6 +620,7 @@ export function findBestPlacement(
);
return {
...breakdown!,
+ total: c.score,
direction: dirNames[c.directionIndex],
ring: c.ring,
};
diff --git a/packages/engine/src/annotations/position.ts b/packages/engine/src/annotations/position.ts
index 1cedace3..a4e2d8e4 100644
--- a/packages/engine/src/annotations/position.ts
+++ b/packages/engine/src/annotations/position.ts
@@ -51,7 +51,7 @@ export function resolvePosition(
const s = scale.scale;
const type = scale.type;
- if (type === 'time') {
+ if (type === 'time' || type === 'utc') {
const date = new Date(String(value));
if (Number.isNaN(date.getTime())) return null;
return (s as ScaleTime)(date);
diff --git a/packages/engine/src/annotations/reference-layout.ts b/packages/engine/src/annotations/reference-layout.ts
index 2c6b2f7f..eac3ae78 100644
--- a/packages/engine/src/annotations/reference-layout.ts
+++ b/packages/engine/src/annotations/reference-layout.ts
@@ -6,52 +6,215 @@ import type {
ThemeColors,
} from '@opendata-ai/openchart-core';
import { overlapArea } from '@opendata-ai/openchart-core';
-import { type AnnotationMeasureTextFn, estimateLabelBounds, refreshConnector } from './geometry';
+import { DEFAULT_LINE_HEIGHT } from './constants';
+import {
+ type AnnotationMeasureTextFn,
+ computeTextBlockBounds,
+ estimateLabelBounds,
+ refreshConnector,
+} from './geometry';
import type { PlacementObstacle } from './placement';
import { wrapAnnotationText } from './wrap-text';
+/** Vertical gap between top-lane rows. */
+export const REFERENCE_LANE_GAP = 8;
+/** Gap between the lowest top-lane row and the plot. */
+export const REFERENCE_LANE_PAD = 24;
+
+/** A vertical outside label measured for the top lane. */
+export interface ReferenceLaneLabel {
+ /** Index of the annotation in the spec's annotation list. */
+ index: number;
+ width: number;
+ height: number;
+}
+
+/** A packed top-lane row: labels whose horizontal extents do not overlap. */
+export interface ReferenceLaneRow {
+ members: number[];
+ height: number;
+}
+
+/** An admitted top-lane row, positioned by the distance from its top to the plot top. */
+export interface ReferenceLaneSlot {
+ members: number[];
+ above: number;
+}
+
export interface ReferenceReserve {
right: number;
+ /** Height reserved above the plot for the top lane. */
top: number;
labelWidth: number;
+ /** Vertical outside labels, measured at `labelWidth`, in source order. */
+ labels: ReferenceLaneLabel[];
+ /**
+ * Admitted top-lane rows. A vertical outside label in no row places inside
+ * the plot. Placement reads these; it never re-derives the lane.
+ */
+ rows: ReferenceLaneSlot[];
+ /** Whether any top-lane row was admitted. */
+ topLane?: boolean;
}
-/** The reserve depends only on authored labels and container width, never placement results. */
+/** No outside reference labels: nothing reserved. */
+export const NO_REFERENCE_RESERVE: ReferenceReserve = {
+ right: 0,
+ top: 0,
+ labelWidth: 0,
+ labels: [],
+ rows: [],
+};
+
+const isOutsideLabel = (a: Annotation): a is RefLineAnnotation =>
+ a.type === 'refline' && a.labelPlacement === 'outside' && !!a.label;
+
+/**
+ * Measure the outside lanes from authored labels and container width, never
+ * placement results. `fontSize` is the theme small size: the same fallback the
+ * resolver renders labels at. Without x positions every vertical label gets
+ * its own top-lane row, all admitted (the facet grid reserves it per panel).
+ * `edgeInset` is the gap between a right-lane label and the container's
+ * right edge; figures pass the chrome padding so the label lines up with the
+ * title, source and brand. The 16px default is the facet panel inset.
+ */
export function referenceLabelReserve(
annotations: Annotation[],
width: number,
measure: AnnotationMeasureTextFn,
+ fontSize: number,
+ edgeInset = 16,
): ReferenceReserve {
- const outside = annotations.filter(
- (a): a is RefLineAnnotation =>
- a.type === 'refline' && a.labelPlacement === 'outside' && !!a.label,
- );
const labelWidth = Math.max(24, Math.min(160, width * 0.25));
- const vertical = outside.filter((a) => a.y === undefined);
- const top = vertical.reduce(
- (height, a) =>
- height +
- wrapAnnotationText(
- a.label!,
- labelWidth,
- { fontSize: a.fontSize ?? 11, fontWeight: a.fontWeight ?? 400 },
- measure,
- ).length *
- (a.fontSize ?? 11) *
- 1.3 +
- 8,
- 0,
- );
- return {
- right: outside.some((a) => a.y !== undefined) ? labelWidth + 32 : 0,
- top: top ? top + 24 : 0,
+ const labels: ReferenceLaneLabel[] = [];
+ // The right lane is as wide as its widest wrapped label, not the wrap cap.
+ let rightWidth = 0;
+ for (const [index, a] of annotations.entries()) {
+ if (!isOutsideLabel(a)) continue;
+ const font = { fontSize: a.fontSize ?? fontSize, fontWeight: a.fontWeight ?? 400 };
+ const lines = wrapAnnotationText(a.label!, labelWidth, font, measure);
+ const size = computeTextBlockBounds(
+ 0,
+ 0,
+ lines.join('\n'),
+ { ...font, lineHeight: DEFAULT_LINE_HEIGHT },
+ measure,
+ );
+ if (a.y !== undefined) {
+ rightWidth = Math.max(rightWidth, Math.min(labelWidth, Math.ceil(size.width)));
+ continue;
+ }
+ labels.push({
+ index,
+ width: size.width,
+ height: lines.length * font.fontSize * DEFAULT_LINE_HEIGHT,
+ });
+ }
+ // placeReferenceLabel starts a right label 24px into the lane, and the
+ // container it measures from is the SVG inset by 8px
+ // (defaultReferenceContainer): the label ends `edgeInset` from the edge.
+ const reserve: ReferenceReserve = {
+ right: rightWidth > 0 ? rightWidth + 24 - 8 + edgeInset : 0,
+ top: 0,
labelWidth,
+ labels,
+ rows: [],
+ };
+ const rows = packReferenceRows(labels);
+ return admitReferenceRows(reserve, rows, referenceLaneHeights(rows).at(-1) ?? 0);
+}
+
+/**
+ * The figure-level container outside labels stay inside: the SVG inset by
+ * 8px, below the top chrome when there is one.
+ */
+export function defaultReferenceContainer(
+ svg: { width: number; height: number },
+ topBound?: number,
+): Rect {
+ return {
+ x: 8,
+ y: topBound ?? 8,
+ width: svg.width - 16,
+ height: svg.height - (topBound ?? 8) - 8,
};
}
+/**
+ * Pack vertical outside labels into top-lane rows, greedy first-fit in source
+ * order: a label joins the first row where its horizontal extent (as placement
+ * will position it, centered on its rule and clamped to the plot and
+ * container) clears every member by REFERENCE_LANE_GAP. Without `xOf` every
+ * label gets its own row. Labels whose rule does not resolve take no row.
+ */
+export function packReferenceRows(
+ labels: ReferenceLaneLabel[],
+ xOf?: (index: number) => number | null,
+ plot?: { x: number },
+ container?: { x: number; width: number },
+): ReferenceLaneRow[] {
+ const rows: Array }> = [];
+ for (const label of labels) {
+ let span: [number, number] | undefined;
+ if (xOf && plot && container) {
+ const x = xOf(label.index);
+ if (x === null) continue;
+ const left = Math.min(
+ container.x + container.width - label.width,
+ Math.max(plot.x, x - label.width / 2),
+ );
+ span = [left, left + label.width];
+ }
+ const row =
+ span &&
+ rows.find((r) =>
+ r.spans.every(
+ ([start, end]) =>
+ span[0] >= end + REFERENCE_LANE_GAP || span[1] + REFERENCE_LANE_GAP <= start,
+ ),
+ );
+ if (row && span) {
+ row.members.push(label.index);
+ row.spans.push(span);
+ row.height = Math.max(row.height, label.height);
+ } else rows.push({ members: [label.index], height: label.height, spans: span ? [span] : [] });
+ }
+ return rows.map(({ members, height }) => ({ members, height }));
+}
+
+/** Cumulative lane height through each row, including the pad to the plot. */
+export function referenceLaneHeights(rows: ReferenceLaneRow[]): number[] {
+ let sum = 0;
+ // Same association as the v8 reserve sum, so stacked lanes keep their exact positions.
+ return rows.map((row) => (sum = sum + row.height + REFERENCE_LANE_GAP) + REFERENCE_LANE_PAD);
+}
+
+/**
+ * Admit the leading rows that fit in `laneReserve` and position them flush
+ * against the plot. Space the admitted rows do not use stays above them.
+ */
+export function admitReferenceRows(
+ reserve: ReferenceReserve,
+ rows: ReferenceLaneRow[],
+ laneReserve: number,
+): ReferenceReserve & { topLane: boolean } {
+ const cumulative = referenceLaneHeights(rows);
+ let admitted = 0;
+ while (admitted < rows.length && cumulative[admitted] <= laneReserve) admitted++;
+ const total = admitted ? cumulative[admitted - 1] : 0;
+ let offset = 0;
+ const slots = rows.slice(0, admitted).map((row) => {
+ const slot = { members: row.members, above: total - offset };
+ offset += row.height + REFERENCE_LANE_GAP;
+ return slot;
+ });
+ return { ...reserve, top: laneReserve, rows: slots, topLane: admitted > 0 };
+}
+
export interface ReferenceLanes {
rightLabels?: ResolvedAnnotation[];
- topBottom?: number;
+ /** A lane overflowed once already; one warning per compile covers every label. */
+ topOverflow?: boolean;
}
/** Opt-in placement leaves the legacy resolver and all un-authored paths untouched. */
@@ -73,19 +236,24 @@ export function placeReferenceLabel(
const horizontal = annotation.y !== undefined;
const initial = { x: label.x, y: label.y };
const bounds = () => estimateLabelBounds(label, measure);
+ // Containment for this label; an outside label that falls back inside is
+ // held to the plot's vertical extent, clear of the lane above it.
+ let region = container;
const fits = (box: Rect) =>
- box.x >= container.x &&
- box.y >= container.y &&
- box.x + box.width <= container.x + container.width &&
- box.y + box.height <= container.y + container.height;
+ box.x >= region.x &&
+ box.y >= region.y &&
+ box.x + box.width <= region.x + region.width &&
+ box.y + box.height <= region.y + region.height;
let target = horizontal ? line.end : line.start;
+ let placement = annotation.labelPlacement;
if (annotation.labelOffset !== undefined) {
if (!fits(bounds()))
warn?.(
'[openchart] Reference labelOffset pins an overflowing label; remove labelOffset to allow automatic containment.',
);
- } else if (annotation.labelPlacement === 'outside') {
+ } else if (placement === 'outside') {
+ const inlineStyle = label.style;
label.lines = wrapAnnotationText(
label.text,
reserve.labelWidth,
@@ -94,15 +262,38 @@ export function placeReferenceLabel(
);
label.style = { ...label.style, textAnchor: 'start' };
const size = bounds();
- if (horizontal) {
+ // The budget's admitted top-lane row for this label, if any.
+ const row =
+ horizontal || reserve.topLane === false || resolved.specIndex === undefined
+ ? undefined
+ : reserve.rows.find((slot) => slot.members.includes(resolved.specIndex!));
+ const top = row ? area.y - row.above : Number.NaN;
+ if (
+ horizontal ? reserve.right === 0 : !row || top < container.y || top + size.height > area.y
+ ) {
+ // No lane for this label (not admitted by the budget, no reserve at all,
+ // or too small a figure): place the wrapped label inside instead.
+ label.style = inlineStyle;
+ placement = 'auto';
+ if (!horizontal) {
+ const y = Math.max(container.y, area.y);
+ const bottom = Math.min(container.y + container.height, area.y + area.height);
+ region = { x: container.x, y, width: container.width, height: Math.max(0, bottom - y) };
+ }
+ if (!lanes.topOverflow)
+ warn?.(
+ '[openchart] Outside reference labels exceed their available lane; use shorter labels or a taller figure.',
+ );
+ lanes.topOverflow = true;
+ } else if (horizontal) {
label.x = container.x + container.width - reserve.right + 24;
const previous = lanes.rightLabels?.at(-1)?.bounds;
- const top = Math.max(
+ const rightTop = Math.max(
area.y,
line.end.y - size.height / 2,
previous ? previous.y + previous.height + 8 : area.y,
);
- label.y += top - bounds().y;
+ label.y += rightTop - bounds().y;
resolved.bounds = bounds();
(lanes.rightLabels ??= []).push(resolved);
// Sweep the stack back from the bottom edge without changing source order.
@@ -118,20 +309,22 @@ export function placeReferenceLabel(
if (item.label!.connector)
item.label!.connector = refreshConnector(item.label!.connector, item.bounds, 0);
}
- if (nextTop < area.y)
+ if (nextTop < area.y && !lanes.topOverflow) {
warn?.(
'[openchart] Outside reference labels exceed their available lane; use shorter labels or a taller figure.',
);
+ lanes.topOverflow = true;
+ }
} else {
+ // Same horizontal extent the budget packed the row with.
label.x = Math.min(
container.x + container.width - size.width,
Math.max(area.x, line.start.x - size.width / 2),
);
- const top = lanes.topBottom ?? area.y - reserve.top;
label.y += top - bounds().y;
- lanes.topBottom = top + size.height + 8;
}
- } else if (annotation.labelPlacement === 'auto') {
+ }
+ if (placement === 'auto' && annotation.labelOffset === undefined) {
const original = { x: label.x, y: label.y, anchor: label.style.textAnchor };
const candidates = horizontal
? [
@@ -161,13 +354,13 @@ export function placeReferenceLabel(
// no pixel scan or side flip is needed. Bound the search for dense plots.
const shifts = horizontal
? [
- container.x - box.x,
- container.x + container.width - box.x - box.width,
+ region.x - box.x,
+ region.x + region.width - box.x - box.width,
...relevant.flatMap((o) => [o.x - box.x - box.width - 4, o.x + o.width - box.x + 4]),
]
: [
- container.y - box.y,
- container.y + container.height - box.y - box.height,
+ region.y - box.y,
+ region.y + region.height - box.y - box.height,
...relevant.flatMap((o) => [o.y - box.y - box.height - 4, o.y + o.height - box.y + 4]),
];
for (const shift of [...new Set(shifts)]
@@ -184,31 +377,24 @@ export function placeReferenceLabel(
label.x = original.x;
label.y = original.y;
const shift = horizontal
- ? Math.max(
- container.x - box.x,
- Math.min(0, container.x + container.width - box.x - box.width),
- )
- : Math.max(
- container.y - box.y,
- Math.min(0, container.y + container.height - box.y - box.height),
- );
+ ? Math.max(region.x - box.x, Math.min(0, region.x + region.width - box.x - box.width))
+ : Math.max(region.y - box.y, Math.min(0, region.y + region.height - box.y - box.height));
if (horizontal) label.x += shift;
else label.y += shift;
- warn?.(
- '[openchart] Reference label has no clear contained position; use labelPlacement: "outside" or shorter text.',
- );
+ // A lane overflow already warned; this advice would point back at 'outside'.
+ if (annotation.labelPlacement === 'auto')
+ warn?.(
+ '[openchart] Reference label has no clear contained position; use labelPlacement: "outside" or shorter text.',
+ );
}
}
}
label.background = colors?.annotationPlate;
label.bounds = bounds();
resolved.bounds = label.bounds;
- if (
- annotation.labelPlacement === 'outside' ||
- Math.hypot(label.x - initial.x, label.y - initial.y) > 8
- ) {
+ if (placement === 'outside' || Math.hypot(label.x - initial.x, label.y - initial.y) > 8) {
// A displaced label points to its rule, never a nearby observation.
- if (annotation.labelPlacement !== 'outside')
+ if (placement !== 'outside')
target = horizontal ? { x: initial.x, y: line.start.y } : { x: line.start.x, y: initial.y };
label.connector = refreshConnector(
{
@@ -220,9 +406,7 @@ export function placeReferenceLabel(
arrow: false,
// The whole leader stays left of the right-gutter text, even when a
// tall neighbor displaces this label. Persist through sweep/tween rebuilds.
- ...(annotation.labelPlacement === 'outside' &&
- horizontal &&
- annotation.labelOffset === undefined
+ ...(placement === 'outside' && horizontal && annotation.labelOffset === undefined
? { labelAttachment: 'left-center' as const }
: {}),
},
diff --git a/packages/engine/src/annotations/resolve-text.ts b/packages/engine/src/annotations/resolve-text.ts
index c2d34b07..8f7d9922 100644
--- a/packages/engine/src/annotations/resolve-text.ts
+++ b/packages/engine/src/annotations/resolve-text.ts
@@ -28,7 +28,6 @@ import {
DEFAULT_LINE_HEIGHT,
DROP_LINE_TOP_GAP,
FALLBACK_FONT_FAMILY,
- LEDE_FONT_WEIGHT,
LIGHT_CONNECTOR_STROKE,
LIGHT_DOT_FILL,
LIGHT_LABEL_BACKGROUND,
@@ -42,13 +41,13 @@ import {
import {
type AnnotationMeasureTextFn,
applyOffset,
- automaticControlPoint,
computeAnchorOffset,
computeTextBlockBounds,
connectorExit,
connectorPullbackGap,
heuristicMeasure,
measureRichLine,
+ pullBackConnector,
unionRects,
} from './geometry';
import { resolvePosition } from './position';
@@ -123,7 +122,7 @@ function resolveConnectorConfig(
if (typeof connector === 'object' && connector !== null && 'type' in connector) {
const cfg = connector as ConnectorConfig;
- const arrow = cfg.type === 'drop-line' ? false : (cfg.arrow ?? cfg.type === 'curve');
+ const arrow = cfg.type === 'drop-line' ? false : (cfg.arrow ?? false);
return {
type: cfg.type,
arrow,
@@ -131,7 +130,7 @@ function resolveConnectorConfig(
};
}
- if (connector === 'curve') return { type: 'curve', arrow: true };
+ if (connector === 'curve') return { type: 'curve', arrow: false };
if (connector === 'drop-line') return { type: 'drop-line', arrow: false };
// true, 'straight', or undefined all map to straight with no arrow
@@ -215,13 +214,9 @@ export function makeAnnotationLabelStyle(
};
}
-/**
- * The primary text's resolved weight. A subtitle turns the primary line into a
- * lede, so it goes bold unless the author asked for a specific weight.
- */
+/** Primary text stays regular unless the author supplies a weight. */
export function resolveLedeFontWeight(annotation: TextAnnotation): number | undefined {
- if (annotation.fontWeight !== undefined) return annotation.fontWeight;
- return annotation.subtitle ? LEDE_FONT_WEIGHT : undefined;
+ return annotation.fontWeight;
}
/** Resolve wrapped display geometry, retaining authored text as the identity. */
@@ -449,14 +444,12 @@ function resolveTextAnnotationDisplay(
};
// Pull the data-point end back so the line stops short of the marker.
- const gap = connectorPullbackGap(markerClearance(dot), connectorArrow);
- const cdx = endpoint.x - from.x;
- const cdy = endpoint.y - from.y;
- const dist = Math.sqrt(cdx * cdx + cdy * cdy);
- const to =
- dist > 0
- ? { x: endpoint.x - (cdx / dist) * gap, y: endpoint.y - (cdy / dist) * gap }
- : { ...endpoint };
+ const { to, control } = pullBackConnector(
+ from,
+ endpoint,
+ connectorPullbackGap(markerClearance(dot), connectorArrow),
+ connectorStyle === 'curve' ? { exit: exit.exit, bend: connectorConfig?.bend } : undefined,
+ );
// Min-length check runs after the pullback: it can flip a near-degenerate
// line's direction, and a stub shorter than MIN_CONNECTOR_LENGTH reads as
@@ -482,7 +475,7 @@ function resolveTextAnnotationDisplay(
exit: exit.exit,
...(connectorStyle === 'curve'
? {
- control: automaticControlPoint(from, to, exit.exit, connectorConfig?.bend),
+ control,
...(connectorConfig?.bend !== undefined ? { bend: connectorConfig.bend } : {}),
}
: {}),
@@ -627,9 +620,7 @@ function resolveDropLineAnnotation(
measure,
);
- // A subtitle works on a drop-line the same as on any other connector style:
- // the lede rule already bolded the primary line on the way in, so dropping the
- // subtitle here would leave a mystery-bold label with no context under it.
+ // A subtitle works on a drop-line the same as on any other connector style.
let subtitle: ResolvedAnnotation['subtitle'] | undefined;
let annotationBounds = labelBounds;
if (annotation.subtitle) {
diff --git a/packages/engine/src/annotations/thinning.ts b/packages/engine/src/annotations/thinning.ts
index 63739447..be755148 100644
--- a/packages/engine/src/annotations/thinning.ts
+++ b/packages/engine/src/annotations/thinning.ts
@@ -162,14 +162,15 @@ export function thinAnnotations(
const overBudget = plotAreaSize > 0 && usedArea + candidateArea > coverageBudget;
if (overLimit || overlaps || !fitsWithin(bounds, containment) || overBudget) {
- const footnoteIndex = footnotes.length + 1;
+ const footnoteIndex = specs
+ .slice(0, (entry.resolved.specIndex ?? entry.index) + 1)
+ .filter((a) => a.type === 'text').length;
footnotes.push({
...(entry.resolved.specIndex !== undefined ? { specIndex: entry.resolved.specIndex } : {}),
...(entry.resolved.id !== undefined ? { id: entry.resolved.id } : {}),
index: footnoteIndex,
- text: entry.spec?.subtitle
- ? `${entry.resolved.label!.text}. ${entry.spec.subtitle}`
- : entry.resolved.label!.text,
+ text: entry.resolved.label!.text,
+ ...(entry.spec?.subtitle ? { subtitle: entry.spec.subtitle } : {}),
});
entry.resolved.footnoteIndex = footnoteIndex;
} else {
@@ -178,7 +179,7 @@ export function thinAnnotations(
}
}
- return { annotations, footnotes };
+ return { annotations, footnotes: footnotes.sort((a, b) => a.index - b.index) };
}
/**
diff --git a/packages/engine/src/barlist/__tests__/compile-barlist.test.ts b/packages/engine/src/barlist/__tests__/compile-barlist.test.ts
index 5db4e0c2..3be6aced 100644
--- a/packages/engine/src/barlist/__tests__/compile-barlist.test.ts
+++ b/packages/engine/src/barlist/__tests__/compile-barlist.test.ts
@@ -114,7 +114,8 @@ describe('compileBarList', () => {
expect(layout.tooltipDescriptors.size).toBe(4);
const tooltip = layout.tooltipDescriptors.get('0');
expect(tooltip?.title).toBe('Alpha');
- expect(tooltip?.fields[0].value).toBeDefined();
+ // One field: the value channel's field name and Alpha's formatted count.
+ expect(tooltip?.fields).toEqual([{ label: 'count', value: '100' }]);
});
it('builds a11y metadata', () => {
@@ -136,8 +137,13 @@ describe('compileBarList', () => {
expect(() => compileBarList(spec, BASE_OPTIONS)).toThrow(/empty/i);
});
- it('formats values with valueFormat', () => {
- const spec = makeSpec({ valueFormat: '$,.0f' });
+ it('formats values with encoding.value.format', () => {
+ const spec = makeSpec({
+ encoding: {
+ ...makeSpec().encoding,
+ value: { ...makeSpec().encoding.value, format: '$,.0f' },
+ },
+ });
const layout = compileBarList(spec, BASE_OPTIONS);
expect(layout.rows[0].formattedValue).toBe('$100');
});
@@ -148,7 +154,7 @@ describe('compileBarList', () => {
{ label: 'Big', count: 1500 },
{ label: 'Small', count: 200 },
],
- valueFormat: '~s',
+ encoding: { ...makeSpec().encoding, value: { ...makeSpec().encoding.value, format: '~s' } },
});
const layout = compileBarList(spec, BASE_OPTIONS);
expect(layout.rows[0].formattedValue).toBe('1.5k');
diff --git a/packages/engine/src/barlist/compile-barlist.ts b/packages/engine/src/barlist/compile-barlist.ts
index c5ddd6c4..5afc5574 100644
--- a/packages/engine/src/barlist/compile-barlist.ts
+++ b/packages/engine/src/barlist/compile-barlist.ts
@@ -29,7 +29,7 @@ import {
estimateTextWidth,
resolveTheme,
} from '@opendata-ai/openchart-core';
-import { emitSpecWarnings, emitThemeWarnings, expandSpecSugar } from '../compile/spec-sugar';
+import { emitSpecWarnings, expandSpecSugar } from '../compile/spec-sugar';
import { resolveAnimation } from '../compiler/animation';
import { compile as compileSpec } from '../compiler/index';
import { finiteNumber } from '../data/finite-number';
@@ -82,7 +82,6 @@ export function compileBarList(spec: unknown, options: CompileOptions): BarListL
rawWatermark !== undefined ? barlistSpec.watermark : (options.watermark ?? true);
// Resolve theme
- emitThemeWarnings(options.theme, options.onWarn);
const mergedThemeConfig = mergeThemeConfig(barlistSpec.theme, options.theme);
const lightTheme: ResolvedTheme = resolveTheme(mergedThemeConfig);
let theme: ResolvedTheme = lightTheme;
@@ -92,13 +91,7 @@ export function compileBarList(spec: unknown, options: CompileOptions): BarListL
// Compute chrome
const chrome = computeChrome(
- {
- title: barlistSpec.chrome.title,
- subtitle: barlistSpec.chrome.subtitle,
- source: barlistSpec.chrome.source,
- byline: barlistSpec.chrome.byline,
- footer: barlistSpec.chrome.footer,
- },
+ barlistSpec.chrome,
theme,
options.width,
options.measureText,
@@ -182,7 +175,7 @@ export function compileBarList(spec: unknown, options: CompileOptions): BarListL
// Value formatter: prefer encoding-level format (v8 canonical), fall back to deprecated valueFormat
const formatter = resolveFieldFormatter({
- surfaceFormat: barlistSpec.encoding.value.format ?? barlistSpec.valueFormat,
+ surfaceFormat: barlistSpec.encoding.value.format,
values: validRows.map((r) => r[valueField]),
});
diff --git a/packages/engine/src/barlist/types.ts b/packages/engine/src/barlist/types.ts
index 25d73c1f..d9583a36 100644
--- a/packages/engine/src/barlist/types.ts
+++ b/packages/engine/src/barlist/types.ts
@@ -5,7 +5,6 @@
import type {
AnimationSpec,
BarListEncoding,
- DarkMode,
DataRow,
ThemeConfig,
} from '@opendata-ai/openchart-core';
@@ -21,8 +20,6 @@ export interface NormalizedBarListSpec {
maxItems: number;
chrome: NormalizedChrome;
theme: ThemeConfig;
- darkMode: DarkMode;
watermark: boolean;
animation?: AnimationSpec;
- valueFormat?: string;
}
diff --git a/packages/engine/src/charts/bar/__tests__/label-plan.test.ts b/packages/engine/src/charts/bar/__tests__/label-plan.test.ts
new file mode 100644
index 00000000..4918899c
--- /dev/null
+++ b/packages/engine/src/charts/bar/__tests__/label-plan.test.ts
@@ -0,0 +1,133 @@
+import { resolveTheme } from '@opendata-ai/openchart-core';
+import { describe, expect, it } from 'vitest';
+import type { NormalizedChartSpec } from '../../../compiler/types';
+import { planBarLabels } from '../label-plan';
+
+function spec(
+ placement: 'column' | 'outside' | 'inside' | 'adaptive',
+ values = [-10, 0, 20],
+): NormalizedChartSpec {
+ return {
+ markType: 'bar',
+ markDef: { type: 'bar' },
+ data: values.map((value, i) => ({ name: `${i}`, value })),
+ encoding: {
+ x: { field: 'value', type: 'quantitative' },
+ y: { field: 'name', type: 'nominal' },
+ },
+ labels: { density: 'all', format: '.1f', placement },
+ chrome: {},
+ annotations: [],
+ responsive: true,
+ theme: {},
+ darkMode: 'off',
+ };
+}
+const theme = resolveTheme();
+describe('pre-layout bar label plan', () => {
+ it('measures every formatted value, including affixes, with the host measurer', () => {
+ const input = spec('column', [100, 0.01]);
+ input.labels.prefix = '$';
+ input.labels.suffix = '%';
+ const texts: string[] = [];
+ const plan = planBarLabels(input, theme, {
+ availableWidth: 400,
+ measureText: (text) => {
+ texts.push(text);
+ return { width: text === '$0.0%' ? 90 : 30, height: 14 };
+ },
+ });
+ expect(texts).toEqual(['$100.0%', '$0.0%']);
+ expect(plan.reserveRight).toBe(102);
+ expect(plan.columnWidth).toBe(90);
+ expect(plan.placement).toBe('column');
+ });
+ it('falls back as one chart before placement and exposes the diagnostic reason', () => {
+ const input = spec('column', [100]);
+ const plan = planBarLabels(input, theme, {
+ availableWidth: 110,
+ measureText: () => ({ width: 40, height: 14 }),
+ });
+ expect(plan.placement).toBe('outside');
+ expect(plan.fallback).toBe('column-to-outside');
+ expect(plan.reserveRight).toBe(46);
+ });
+ it('suppresses as a whole if even outside cannot retain the minimum plot', () => {
+ const plan = planBarLabels(spec('column'), theme, {
+ availableWidth: 65,
+ measureText: () => ({ width: 80, height: 14 }),
+ });
+ expect(plan.placement).toBe('suppress');
+ expect(plan.fallback).toBe('labels-suppressed');
+ expect(plan.reserveRight).toBe(0);
+ });
+ it('outside reserves both signed ends and zero uses the positive side', () => {
+ const plan = planBarLabels(spec('outside'), theme, {
+ availableWidth: 400,
+ measureText: () => ({ width: 40, height: 14 }),
+ });
+ expect(plan.reserveLeft).toBe(46);
+ expect(plan.reserveRight).toBe(46);
+ });
+ it('omission and adaptive keep the established zero-reservation behavior', () => {
+ const input = spec('adaptive');
+ delete input.labels.placement;
+ expect(planBarLabels(input, theme, { availableWidth: 65 }).reserveRight).toBe(0);
+ expect(planBarLabels(input, theme, { availableWidth: 65 }).placement).toBe('adaptive');
+ });
+ it('a figure-level column width aligns facet reservations', () => {
+ const options = { availableWidth: 300, columnWidth: 100 };
+ expect(planBarLabels(spec('column', [1]), theme, options).reserveRight).toBe(112);
+ expect(planBarLabels(spec('column', [1000]), theme, options).reserveRight).toBe(112);
+ });
+});
+
+for (const width of [330, 360, 680]) {
+ for (const fontSize of [12, 14]) {
+ it(`measured gutter preserves ${fontSize}px labels at ${width}px`, () => {
+ const input = spec('column', [1, 100]);
+ input.labels.fontSize = fontSize;
+ const plan = planBarLabels(input, theme, {
+ availableWidth: width,
+ measureText: (text, size) => ({ width: text.length * size, height: size * 1.2 }),
+ });
+ expect(plan.placement).toBe('column');
+ expect(plan.reserveRight).toBe(5 * fontSize + 12);
+ });
+ }
+}
+
+it('vertical bars reserve top/bottom space without inventing a value column', () => {
+ const input = spec('outside');
+ input.encoding = {
+ x: { field: 'name', type: 'nominal' },
+ y: { field: 'value', type: 'quantitative' },
+ };
+ const plan = planBarLabels(input, theme, {
+ availableWidth: 400,
+ availableHeight: 300,
+ measureText: () => ({ width: 40, height: 16 }),
+ });
+ expect(plan.reserveRight).toBe(20);
+ expect(plan.reserveLeft).toBe(20);
+ expect(plan.columnWidth).toBe(0);
+ expect(plan.reserveTop).toBe(24);
+ expect(plan.reserveBottom).toBe(24);
+});
+
+it('does not reserve or format missing observations as true zero', () => {
+ const input = spec('column');
+ input.data = [null, undefined, '', ' ', false, Infinity, NaN, 0].map((value, i) => ({
+ name: `${i}`,
+ value,
+ }));
+ const seen: string[] = [];
+ planBarLabels(input, theme, {
+ availableWidth: 400,
+ measureText: (text) => {
+ seen.push(text);
+ return { width: 20, height: 12 };
+ },
+ });
+ expect(seen).toEqual(['0.0']);
+});
diff --git a/packages/engine/src/charts/bar/__tests__/labels.test.ts b/packages/engine/src/charts/bar/__tests__/labels.test.ts
index a3d20b0a..89323f56 100644
--- a/packages/engine/src/charts/bar/__tests__/labels.test.ts
+++ b/packages/engine/src/charts/bar/__tests__/labels.test.ts
@@ -308,3 +308,122 @@ describe('computeBarLabels knockout halo', () => {
expect(labels[0].halo).not.toBe(false);
});
});
+
+describe('explicit bar label placement', () => {
+ const options = (placement: 'inside' | 'outside' | 'column') => ({
+ placement,
+ measureText: () => ({ width: 80, height: 14 }),
+ columnWidth: 80,
+ });
+ it('inside hides text that cannot fit a bar even when above the adaptive threshold', () => {
+ const mark = makeMark(0, 10); // 50px wide, too short for 80px text
+ const label = computeBarLabels(
+ [mark],
+ chartArea,
+ 'all',
+ null,
+ undefined,
+ 'value',
+ undefined,
+ false,
+ undefined,
+ undefined,
+ 'Georgia',
+ options('inside'),
+ )[0];
+ expect(label.visible).toBe(false);
+ });
+ it('outside follows the raw signed value, including format strings without a minus', () => {
+ const signed = [makeMark(0, -10), makeMark(1, 0), makeMark(2, 20)];
+ const labels = computeBarLabels(
+ signed,
+ chartArea,
+ 'all',
+ () => 'ten',
+ undefined,
+ 'value',
+ undefined,
+ false,
+ undefined,
+ undefined,
+ 'Georgia',
+ options('outside'),
+ );
+ expect(labels[0].x).toBe(signed[0].x - 6);
+ expect(labels[0].style.textAnchor).toBe('end');
+ expect(labels[1].style.textAnchor).toBe('start');
+ expect(labels[2].x).toBe(signed[2].x + signed[2].width + 6);
+ });
+ it('column aligns every signed value at the measured gutter edge', () => {
+ const labels = computeBarLabels(
+ [makeMark(0, -10), makeMark(1, 0), makeMark(2, 20)],
+ chartArea,
+ 'all',
+ null,
+ undefined,
+ 'value',
+ undefined,
+ false,
+ undefined,
+ undefined,
+ 'Georgia',
+ options('column'),
+ );
+ expect(labels.map((label) => label.x)).toEqual([492, 492, 492]);
+ expect(labels.every((label) => label.style.textAnchor === 'end')).toBe(true);
+ });
+});
+
+it('explicit adaptive preserves omitted placement geometry', () => {
+ const input = [makeMark(0, -10), makeMark(1, 0), makeMark(2, 20)];
+ const omitted = computeBarLabels(input, chartArea, 'all', null, undefined, 'value');
+ const adaptive = computeBarLabels(
+ input,
+ chartArea,
+ 'all',
+ null,
+ undefined,
+ 'value',
+ undefined,
+ false,
+ undefined,
+ undefined,
+ undefined,
+ { placement: 'adaptive' },
+ );
+ expect(adaptive).toEqual(omitted);
+});
+
+it('adaptive measures large text before choosing an inside anchor', () => {
+ const mark = makeMark(0, 10);
+ const label = computeBarLabels(
+ [mark],
+ chartArea,
+ 'all',
+ () => '350M',
+ undefined,
+ 'value',
+ undefined,
+ false,
+ 26,
+ )[0];
+ expect(label.style.textAnchor).toBe('start');
+ expect(label.x).toBe(mark.x + mark.width + 6);
+ expect(label.visible).toBe(true);
+});
+
+it('adaptive suppresses a label when neither inside nor its outside endpoint can fit', () => {
+ const mark = makeMark(0, 80);
+ const label = computeBarLabels(
+ [mark],
+ chartArea,
+ 'all',
+ () => 'An exceptionally long value that cannot fit into this chart',
+ undefined,
+ 'value',
+ undefined,
+ false,
+ 26,
+ )[0];
+ expect(label.visible).toBe(false);
+});
diff --git a/packages/engine/src/charts/bar/__tests__/layout-capacity.test.ts b/packages/engine/src/charts/bar/__tests__/layout-capacity.test.ts
new file mode 100644
index 00000000..4c16c996
--- /dev/null
+++ b/packages/engine/src/charts/bar/__tests__/layout-capacity.test.ts
@@ -0,0 +1,103 @@
+import type { DesignDiagnostic } from '@opendata-ai/openchart-core';
+import { describe, expect, it } from 'vitest';
+import { compileChart } from '../../../compile';
+
+const columns = {
+ mark: 'bar',
+ data: [
+ { name: 'A very long category label that rotates', value: 20 },
+ { name: 'Another very long category label that rotates', value: -5 },
+ ],
+ encoding: { x: { field: 'name', type: 'nominal' }, y: { field: 'value', type: 'quantitative' } },
+};
+
+describe('value-label capacity uses the actual measured figure frame', () => {
+ for (const width of [330, 390]) {
+ for (const height of [250, 300]) {
+ it(`suppresses outside labels before reserving an unusable plot at ${width}x${height}`, () => {
+ const diagnostics: DesignDiagnostic[] = [];
+ const base = compileChart({ ...columns, labels: { fontSize: 32 } }, { width, height });
+ const next = compileChart(
+ { ...columns, labels: { fontSize: 32, placement: 'outside' } },
+ { width, height, onDiagnostic: (diagnostic) => diagnostics.push(diagnostic) },
+ );
+ expect(next.area).toEqual(base.area);
+ expect(next.marks.every((mark) => mark.type !== 'rect' || !mark.label)).toBe(true);
+ expect(
+ diagnostics.some(
+ (diagnostic) =>
+ diagnostic.code === 'layout.value-labels.labels-suppressed' &&
+ diagnostic.path === '/labels/placement',
+ ),
+ ).toBe(true);
+ });
+ }
+ }
+ it('retains outside labels when the complete frame has at least the minimum usable plot', () => {
+ const layout = compileChart(
+ { ...columns, labels: { fontSize: 32, placement: 'outside' } },
+ { width: 390, height: 500 },
+ );
+ expect(layout.area.height).toBeGreaterThanOrEqual(60);
+ expect(layout.marks.some((mark) => mark.type === 'rect' && mark.label?.visible)).toBe(true);
+ });
+});
+
+it('includes authored header and chrome bands in outside-label capacity', () => {
+ const spec = {
+ ...columns,
+ chromeLayout: 'subtract' as const,
+ chrome: {
+ title: 'A fixed-height figure with a measured header',
+ subtitle: 'The title, subtitle and footer keep their reservations',
+ source: 'Illustrative data',
+ },
+ encoding: {
+ ...columns.encoding,
+ y: {
+ ...columns.encoding.y,
+ axis: { title: 'Measured units', titleAngle: 0, titleAnchor: 'end' },
+ },
+ },
+ };
+ const diagnostics: DesignDiagnostic[] = [];
+ const base = compileChart({ ...spec, labels: { fontSize: 32 } }, { width: 390, height: 400 });
+ const layout = compileChart(
+ { ...spec, labels: { placement: 'outside', fontSize: 32 } },
+ { width: 390, height: 400, onDiagnostic: (diagnostic) => diagnostics.push(diagnostic) },
+ );
+ expect(layout.area).toEqual(base.area);
+ expect(layout.marks.every((mark) => mark.type !== 'rect' || !mark.label)).toBe(true);
+ expect(
+ diagnostics.some((diagnostic) => diagnostic.code === 'layout.value-labels.labels-suppressed'),
+ ).toBe(true);
+});
+
+it.each([
+ 400, 440, 480, 520, 560, 600,
+])('faceted outside labels retain the minimum plot after each measured header at %ipx', (height) => {
+ const layout = compileChart(
+ {
+ ...columns,
+ height,
+ data: columns.data.flatMap((row) => [
+ { ...row, region: 'North' },
+ { ...row, region: 'South' },
+ ]),
+ encoding: {
+ ...columns.encoding,
+ row: { field: 'region', type: 'nominal' },
+ y: {
+ ...columns.encoding.y,
+ axis: { title: 'Measured units', titleAngle: 0, titleAnchor: 'end' },
+ },
+ },
+ labels: { placement: 'outside', fontSize: 32 },
+ },
+ { width: 390, height },
+ );
+ for (const panel of layout.facet!.panels) {
+ if (panel.marks.some((mark) => mark.type === 'rect' && mark.label))
+ expect(panel.area.height).toBeGreaterThanOrEqual(60);
+ }
+});
diff --git a/packages/engine/src/charts/bar/__tests__/theme-font.test.ts b/packages/engine/src/charts/bar/__tests__/theme-font.test.ts
new file mode 100644
index 00000000..37856c6d
--- /dev/null
+++ b/packages/engine/src/charts/bar/__tests__/theme-font.test.ts
@@ -0,0 +1,46 @@
+import type { ChartSpec } from '@opendata-ai/openchart-core';
+import { describe, expect, it } from 'vitest';
+import { compileChart } from '../../../compile';
+
+describe('value label theme font', () => {
+ for (const kind of ['bar', 'column', 'dot', 'range', 'pie'] as const) {
+ it(`${kind} resolves its value labels from the theme`, () => {
+ const horizontal = kind === 'bar' || kind === 'dot' || kind === 'range';
+ const spec = {
+ mark:
+ kind === 'column' ? 'bar' : kind === 'pie' ? 'arc' : kind === 'dot' ? 'lollipop' : kind,
+ data: [
+ { name: 'A', value: 30, end: 60 },
+ { name: 'B', value: 50, end: 70 },
+ ],
+ encoding:
+ kind === 'pie'
+ ? {
+ theta: { field: 'value', type: 'quantitative' },
+ color: { field: 'name', type: 'nominal' },
+ }
+ : {
+ x: {
+ field: horizontal ? 'value' : 'name',
+ type: horizontal ? 'quantitative' : 'nominal',
+ },
+ y: {
+ field: horizontal ? 'name' : 'value',
+ type: horizontal ? 'nominal' : 'quantitative',
+ },
+ ...(kind === 'range' ? { x2: { field: 'end', type: 'quantitative' } } : {}),
+ },
+ labels: { density: 'all' },
+ theme: { fonts: { family: 'Georgia, serif' } },
+ } as ChartSpec;
+ const layout = compileChart(spec, { width: 680, height: 400 });
+ const labels = layout.marks.flatMap((mark) =>
+ 'label' in mark && mark.label ? [mark.label] : [],
+ );
+ expect(labels.length).toBeGreaterThan(0);
+ expect(labels.every((label) => label.style.fontFamily === layout.theme.fonts.family)).toBe(
+ true,
+ );
+ });
+ }
+});
diff --git a/packages/engine/src/charts/bar/index.ts b/packages/engine/src/charts/bar/index.ts
index c83f93f5..e1bcc9b9 100644
--- a/packages/engine/src/charts/bar/index.ts
+++ b/packages/engine/src/charts/bar/index.ts
@@ -6,6 +6,7 @@
import type { Mark } from '@opendata-ai/openchart-core';
import { resolveFieldFormatter } from '../../format/field-format';
+import { filterByDensity } from '../_shared/density-filter';
import type { ChartRenderer } from '../registry';
import { computeBarMarks } from './compute';
import { computeBarLabels } from './labels';
@@ -21,6 +22,8 @@ export const barRenderer: ChartRenderer = (
strategy,
theme,
containerWidth,
+ labelPlan,
+ measureText,
) => {
const marks = computeBarMarks(spec, scales, chartArea, strategy, containerWidth, theme);
@@ -44,9 +47,16 @@ export const barRenderer: ChartRenderer = (
theme.isDark,
spec.labels.fontSize,
spec.labels.suffix,
+ theme.fonts.family,
+ {
+ placement: labelPlan?.placement ?? spec.labels.placement,
+ columnWidth: labelPlan?.columnWidth,
+ measureText: spec.labels.placement ? measureText : undefined,
+ },
);
- for (let i = 0; i < marks.length && i < labels.length; i++) {
- marks[i].label = labels[i];
+ const labelMarks = spec.labels.placement ? filterByDensity(marks, spec.labels.density) : marks;
+ for (let i = 0; i < labelMarks.length && i < labels.length; i++) {
+ labelMarks[i].label = labels[i];
}
return marks as Mark[];
diff --git a/packages/engine/src/charts/bar/label-plan.ts b/packages/engine/src/charts/bar/label-plan.ts
new file mode 100644
index 00000000..8fd2e70e
--- /dev/null
+++ b/packages/engine/src/charts/bar/label-plan.ts
@@ -0,0 +1,155 @@
+/** Decisions made before scales/placement; frame owns all resulting reservations. */
+import type { MeasureTextFn, ResolvedTheme } from '@opendata-ai/openchart-core';
+import { estimateTextWidth } from '@opendata-ai/openchart-core';
+import type { NormalizedChartSpec } from '../../compiler/types';
+import { finiteNumber } from '../../data/finite-number';
+import { resolveFieldFormatter } from '../../format/field-format';
+import { resolveRendererKey } from '../post-process';
+
+export type BarLabelPlacement = 'adaptive' | 'inside' | 'outside' | 'column' | 'suppress';
+export interface BarLabelPlan {
+ placement: BarLabelPlacement;
+ reserveLeft: number;
+ reserveRight: number;
+ reserveTop: number;
+ reserveBottom: number;
+ columnWidth: number;
+ fallback?: 'column-to-outside' | 'labels-suppressed';
+}
+export interface BarLabelPlanOptions {
+ /** Plot width before value-label reservations, after category/axis gutters. */
+ availableWidth: number;
+ availableHeight?: number;
+ /** Actual candidate plot height measured by the shared frame, after every band. */
+ plotHeightForPlan?: (plan: BarLabelPlan) => number;
+ minPlotWidth?: number;
+ minPlotHeight?: number;
+ measureText?: MeasureTextFn;
+ /** Shared figure measurement for aligned facet value columns. */
+ columnWidth?: number;
+}
+
+export function planBarLabels(
+ spec: NormalizedChartSpec,
+ _theme: ResolvedTheme,
+ options: BarLabelPlanOptions,
+): BarLabelPlan {
+ const requested = spec.labels.placement ?? 'adaptive';
+ const empty: BarLabelPlan = {
+ placement: requested,
+ reserveLeft: 0,
+ reserveRight: 0,
+ reserveTop: 0,
+ reserveBottom: 0,
+ columnWidth: 0,
+ };
+ if (requested === 'adaptive' || requested === 'inside' || spec.labels.density === 'none')
+ return empty;
+ const horizontal = resolveRendererKey(spec.markType, spec.encoding, spec.markDef) === 'bar';
+ const channel = horizontal ? spec.encoding.x : spec.encoding.y;
+ if (!channel || !('field' in channel)) return empty;
+ const field = channel.field;
+ const values = spec.data
+ .map((row) => finiteNumber(row[field]))
+ .filter((value): value is number => value !== undefined);
+ const format = resolveFieldFormatter({
+ surfaceFormat: spec.labels.format,
+ channelFormat: 'format' in channel ? channel.format : undefined,
+ values,
+ });
+ const fontSize = spec.labels.fontSize ?? (horizontal ? 11 : 10);
+ let maxWidth = 0;
+ let maxHeight = fontSize * 1.2;
+ let negativeWidth = 0;
+ let positiveWidth = 0;
+ let negative = false;
+ let positive = false;
+ for (const value of values) {
+ const number = Number(value);
+ const text = `${spec.labels.prefix ?? ''}${format(number)}${spec.labels.suffix ?? ''}`;
+ const measured = options.measureText?.(text, fontSize, 600) ?? {
+ width: estimateTextWidth(text, fontSize, 600),
+ height: fontSize * 1.2,
+ };
+ maxWidth = Math.max(maxWidth, measured.width);
+ maxHeight = Math.max(maxHeight, measured.height);
+ if (number < 0) {
+ negative = true;
+ negativeWidth = Math.max(negativeWidth, measured.width);
+ } else {
+ positive = true;
+ positiveWidth = Math.max(positiveWidth, measured.width);
+ }
+ }
+ maxWidth = Math.max(maxWidth, options.columnWidth ?? 0);
+ if (values.length === 0) return empty;
+ const minWidth = options.minPlotWidth ?? 60;
+ const minHeight = options.minPlotHeight ?? 60;
+ const fits = (candidate: BarLabelPlan) =>
+ options.availableWidth - candidate.reserveLeft - candidate.reserveRight >= minWidth &&
+ (options.availableHeight === undefined ||
+ options.availableHeight - candidate.reserveTop - candidate.reserveBottom >= minHeight) &&
+ (options.plotHeightForPlan === undefined || options.plotHeightForPlan(candidate) >= minHeight);
+ const column = {
+ ...empty,
+ placement: 'column' as const,
+ reserveRight: maxWidth + 12,
+ columnWidth: maxWidth,
+ };
+ if (requested === 'column' && fits(column)) return column;
+ const outside = {
+ ...empty,
+ placement: 'outside' as const,
+ reserveLeft: horizontal ? (negative ? negativeWidth + 6 : 0) : maxWidth / 2,
+ reserveRight: horizontal ? (positive ? positiveWidth + 6 : 0) : maxWidth / 2,
+ reserveTop: !horizontal && positive ? maxHeight + 8 : 0,
+ reserveBottom: !horizontal && negative ? maxHeight + 8 : 0,
+ ...(requested === 'column' ? { fallback: 'column-to-outside' as const } : {}),
+ };
+ return fits(outside)
+ ? outside
+ : { ...empty, placement: 'suppress', fallback: 'labels-suppressed' };
+}
+
+/** Keep axis text outside the value-label lane without changing scale/grid positions. */
+export function offsetValueLabelAxes(
+ axes: {
+ x?: import('@opendata-ai/openchart-core').AxisLayout;
+ y?: import('@opendata-ai/openchart-core').AxisLayout;
+ },
+ plan?: BarLabelPlan,
+): void {
+ if (!plan) return;
+ if (axes.x && plan.reserveBottom > 0) {
+ axes.x.start.y += plan.reserveBottom;
+ axes.x.end.y += plan.reserveBottom;
+ if (axes.x.titlePosition) axes.x.titlePosition.y += plan.reserveBottom;
+ }
+ if (axes.y && plan.reserveLeft > 0) {
+ axes.y.start.x -= plan.reserveLeft;
+ axes.y.end.x -= plan.reserveLeft;
+ if (axes.y.titlePosition) axes.y.titlePosition.x -= plan.reserveLeft;
+ }
+}
+
+export function emitValueLabelDiagnostic(
+ plan: BarLabelPlan | undefined,
+ options: Pick<
+ import('@opendata-ai/openchart-core').CompileOptions,
+ 'width' | 'height' | 'onDiagnostic'
+ >,
+): void {
+ if (!plan?.fallback) return;
+ options.onDiagnostic?.({
+ code: `layout.value-labels.${plan.fallback}`,
+ severity: 'warning',
+ status: 'failed',
+ path: '/labels/placement',
+ viewport: { width: options.width, height: options.height ?? 0 },
+ origin: 'author',
+ message:
+ plan.fallback === 'column-to-outside'
+ ? 'The value column cannot retain the minimum plot width; all values use outside placement.'
+ : 'Value labels cannot fit while retaining the minimum plot size; values remain available in accessible data and tooltips.',
+ });
+}
diff --git a/packages/engine/src/charts/bar/labels.ts b/packages/engine/src/charts/bar/labels.ts
index e14413f2..c483c8cb 100644
--- a/packages/engine/src/charts/bar/labels.ts
+++ b/packages/engine/src/charts/bar/labels.ts
@@ -16,6 +16,7 @@
import type {
LabelCandidate,
LabelDensity,
+ MeasureTextFn,
NumberFormatter,
RectMark,
ResolvedLabel,
@@ -28,6 +29,7 @@ import {
} from '@opendata-ai/openchart-core';
import { filterByDensity } from '../_shared/density-filter';
import { formatLabelValue } from '../_shared/format-label-value';
+import type { BarLabelPlacement } from './label-plan';
// ---------------------------------------------------------------------------
// Helpers
@@ -100,7 +102,15 @@ export function computeBarLabels(
darkMode = false,
fontSize?: number,
labelSuffix?: string,
+ fontFamily = 'system-ui, -apple-system, sans-serif',
+ options: {
+ placement?: BarLabelPlacement;
+ columnWidth?: number;
+ measureText?: MeasureTextFn;
+ } = {},
): ResolvedLabel[] {
+ if (options.placement === 'suppress') return [];
+ const placement = options.placement ?? 'adaptive';
const FONT_SIZE = fontSize ?? LABEL_FONT_SIZE;
const targetMarks = filterByDensity(marks, density);
@@ -145,14 +155,19 @@ export function computeBarLabels(
if (labelPrefix) valuePart = labelPrefix + valuePart;
if (labelSuffix) valuePart = valuePart + labelSuffix;
- const textWidth = estimateTextWidth(valuePart, FONT_SIZE, LABEL_FONT_WEIGHT);
- const textHeight = FONT_SIZE * 1.2;
+ const measured = options.measureText?.(valuePart, FONT_SIZE, LABEL_FONT_WEIGHT);
+ const textWidth = measured?.width ?? estimateTextWidth(valuePart, FONT_SIZE, LABEL_FONT_WEIGHT);
+ const textHeight = Math.max(FONT_SIZE * 1.2, measured?.height ?? 0);
// Detect stacked bars: cornerRadius 0 indicates stacked segment
const isStacked = mark.stackGroup !== undefined;
// Determine if label goes inside or outside the bar
- const isInside = mark.width >= MIN_WIDTH_FOR_INSIDE_LABEL;
+ const isInside =
+ placement === 'inside' ||
+ (placement === 'adaptive' &&
+ mark.width >= MIN_WIDTH_FOR_INSIDE_LABEL &&
+ textWidth <= mark.width - 2 * LABEL_PADDING);
const isNegative = Number.isFinite(rawNum) ? rawNum < 0 : false;
const bgColor = getRepresentativeColor(mark.fill);
@@ -160,7 +175,11 @@ export function computeBarLabels(
let fill: string;
let textAnchor: 'start' | 'end' | 'middle';
- if (isStacked && isInside) {
+ if (placement === 'column') {
+ anchorX = chartArea.x + chartArea.width + 12 + (options.columnWidth ?? textWidth);
+ fill = labelColor ?? bgColor;
+ textAnchor = 'end';
+ } else if (isStacked && isInside) {
// Stacked: centered within segment
anchorX = mark.x + mark.width / 2;
fill = pickLabelColor(bgColor, darkMode);
@@ -212,7 +231,15 @@ export function computeBarLabels(
: anchorX;
// Check if label text fits within the stacked segment
- const fits = !(isStacked && textWidth > mark.width - 2 * LABEL_PADDING);
+ const adaptiveOutsideFits =
+ placement !== 'adaptive' ||
+ isInside ||
+ (leftEdgeX >= chartArea.x && leftEdgeX + textWidth <= chartArea.x + chartArea.width);
+ const fits =
+ adaptiveOutsideFits &&
+ (placement === 'inside'
+ ? textWidth <= mark.width - 2 * LABEL_PADDING && textHeight <= mark.height
+ : !(isStacked && placement === 'adaptive' && textWidth > mark.width - 2 * LABEL_PADDING));
// An inside label sits ON the bar fill, contrast-picked against it. A
// surface-colored knockout stroke would ring every glyph, so only labels
@@ -229,7 +256,7 @@ export function computeBarLabels(
height: textHeight,
priority: 'data',
style: {
- fontFamily: 'system-ui, -apple-system, sans-serif',
+ fontFamily,
fontSize: FONT_SIZE,
fontWeight: LABEL_FONT_WEIGHT,
fill,
@@ -268,11 +295,10 @@ export function computeBarLabels(
// Shift resolved y back from top-edge space to the vertical center that the
// central-baseline renderer expects (see anchorY derivation above).
- const halfHeight = FONT_SIZE * 1.2 * 0.5;
-
// Re-insert hidden labels for candidates that didn't fit, preserving order
const results: ResolvedLabel[] = [];
for (let i = 0; i < candidates.length; i++) {
+ const halfHeight = candidates[i].height / 2;
if (unfittingIndices.has(i)) {
results.push({
text: candidates[i].text,
diff --git a/packages/engine/src/charts/binned-bar/__tests__/compute.test.ts b/packages/engine/src/charts/binned-bar/__tests__/compute.test.ts
index 1fde2759..2eaa4495 100644
--- a/packages/engine/src/charts/binned-bar/__tests__/compute.test.ts
+++ b/packages/engine/src/charts/binned-bar/__tests__/compute.test.ts
@@ -196,8 +196,17 @@ describe('binned bar (histogram)', () => {
expect(tooltips).toHaveLength(2);
expect(tooltips[1].title).toBeUndefined();
expect(tooltips[1].fields).toEqual([
- { label: 'Contribution size', value: '$1,000 or more' },
- { label: 'Share', value: '<0.1%' },
+ {
+ label: 'Contribution size',
+ value: '$1,000 or more',
+ source: { field: 'range', channel: 'tooltip', tooltipIndex: 0 },
+ },
+ {
+ label: 'Share',
+ value: '<0.1%',
+ role: 'value',
+ source: { field: 'share', channel: 'tooltip', tooltipIndex: 1 },
+ },
]);
expect(tooltips[0].fields[0].value).toBe('Below $1,000');
});
diff --git a/packages/engine/src/charts/column/__tests__/compute.test.ts b/packages/engine/src/charts/column/__tests__/compute.test.ts
index 4e13555e..1e6fdcda 100644
--- a/packages/engine/src/charts/column/__tests__/compute.test.ts
+++ b/packages/engine/src/charts/column/__tests__/compute.test.ts
@@ -534,8 +534,9 @@ describe('stack modes', () => {
expect(marks.length).toBe(4);
// All stacked marks should have stackGroup set
+ // Segments stack per category: the group is the x value.
for (const mark of marks) {
- expect(mark.stackGroup).toBeDefined();
+ expect(mark.stackGroup).toBe(mark.data.cat);
}
});
@@ -564,8 +565,9 @@ describe('stack modes', () => {
const marks = computeColumnMarks(spec, scales, chartArea, fullStrategy);
expect(marks.length).toBe(4);
+ // Segments stack per category: the group is the x value.
for (const mark of marks) {
- expect(mark.stackGroup).toBeDefined();
+ expect(mark.stackGroup).toBe(mark.data.cat);
}
});
});
diff --git a/packages/engine/src/charts/column/__tests__/labels.test.ts b/packages/engine/src/charts/column/__tests__/labels.test.ts
index 64f19c14..9fe2698c 100644
--- a/packages/engine/src/charts/column/__tests__/labels.test.ts
+++ b/packages/engine/src/charts/column/__tests__/labels.test.ts
@@ -208,3 +208,87 @@ describe('computeColumnLabels knockout halo', () => {
}
});
});
+
+describe('explicit column label placement', () => {
+ it('inside hides labels that do not fit and honors raw negative provenance', () => {
+ const marks: RectMark[] = [
+ {
+ type: 'rect',
+ x: 20,
+ y: 20,
+ width: 30,
+ height: 60,
+ fill: '#4682b4',
+ data: { value: -20 },
+ aria: { label: 'A: -20' },
+ },
+ ];
+ const label = computeColumnLabels(
+ marks,
+ { x: 0, y: 0, width: 400, height: 300 },
+ 'all',
+ () => 'unsigned',
+ undefined,
+ 'value',
+ undefined,
+ false,
+ undefined,
+ undefined,
+ 'Georgia',
+ { placement: 'inside', measureText: () => ({ width: 80, height: 14 }) },
+ )[0];
+ expect(label.visible).toBe(false);
+ });
+ it('outside places a negative label below its mark even when formatting removes sign', () => {
+ const mark: RectMark = {
+ type: 'rect',
+ x: 20,
+ y: 20,
+ width: 80,
+ height: 60,
+ fill: '#4682b4',
+ data: { value: -20 },
+ aria: { label: 'A: -20' },
+ };
+ const label = computeColumnLabels(
+ [mark],
+ { x: 0, y: 0, width: 400, height: 300 },
+ 'all',
+ () => 'unsigned',
+ undefined,
+ 'value',
+ undefined,
+ false,
+ undefined,
+ undefined,
+ 'Georgia',
+ { placement: 'outside' },
+ )[0];
+ expect(label.y).toBeGreaterThan(mark.y + mark.height);
+ expect(label.halo).not.toBe(false);
+ });
+});
+
+it('adaptive negative columns follow the raw value with Unicode minus or unsigned formatting', () => {
+ const mark: RectMark = {
+ type: 'rect',
+ x: 20,
+ y: 20,
+ width: 80,
+ height: 60,
+ fill: '#4682b4',
+ data: { value: -20 },
+ aria: { label: 'A: -20' },
+ };
+ for (const format of [() => '−20', () => 'unsigned']) {
+ const label = computeColumnLabels(
+ [mark],
+ { x: 0, y: 0, width: 400, height: 300 },
+ 'all',
+ format,
+ undefined,
+ 'value',
+ )[0];
+ expect(label.y).toBeGreaterThan(mark.y + mark.height);
+ }
+});
diff --git a/packages/engine/src/charts/column/index.ts b/packages/engine/src/charts/column/index.ts
index 1580d38a..9171c136 100644
--- a/packages/engine/src/charts/column/index.ts
+++ b/packages/engine/src/charts/column/index.ts
@@ -6,6 +6,7 @@
import type { Mark } from '@opendata-ai/openchart-core';
import { resolveFieldFormatter } from '../../format/field-format';
+import { filterByDensity } from '../_shared/density-filter';
import type { ChartRenderer } from '../registry';
import { computeColumnMarks } from './compute';
import { computeColumnLabels } from './labels';
@@ -14,7 +15,16 @@ import { computeColumnLabels } from './labels';
// Column chart renderer
// ---------------------------------------------------------------------------
-export const columnRenderer: ChartRenderer = (spec, scales, chartArea, strategy, theme) => {
+export const columnRenderer: ChartRenderer = (
+ spec,
+ scales,
+ chartArea,
+ strategy,
+ theme,
+ _containerWidth,
+ labelPlan,
+ measureText,
+) => {
const marks = computeColumnMarks(spec, scales, chartArea, strategy, theme);
// Compute and attach value labels (respects spec.labels.density)
@@ -37,9 +47,16 @@ export const columnRenderer: ChartRenderer = (spec, scales, chartArea, strategy,
theme.isDark,
spec.labels.fontSize,
spec.labels.suffix,
+ theme.fonts.family,
+ {
+ placement: labelPlan?.placement ?? spec.labels.placement,
+ columnWidth: labelPlan?.columnWidth,
+ measureText: spec.labels.placement ? measureText : undefined,
+ },
);
- for (let i = 0; i < marks.length && i < labels.length; i++) {
- marks[i].label = labels[i];
+ const labelMarks = spec.labels.placement ? filterByDensity(marks, spec.labels.density) : marks;
+ for (let i = 0; i < labelMarks.length && i < labels.length; i++) {
+ labelMarks[i].label = labels[i];
}
return marks as Mark[];
diff --git a/packages/engine/src/charts/column/labels.ts b/packages/engine/src/charts/column/labels.ts
index 83a5ceac..80d96817 100644
--- a/packages/engine/src/charts/column/labels.ts
+++ b/packages/engine/src/charts/column/labels.ts
@@ -19,6 +19,7 @@
import type {
LabelCandidate,
LabelDensity,
+ MeasureTextFn,
NumberFormatter,
RectMark,
ResolvedLabel,
@@ -32,6 +33,7 @@ import {
} from '@opendata-ai/openchart-core';
import { filterByDensity } from '../_shared/density-filter';
import { formatLabelValue } from '../_shared/format-label-value';
+import type { BarLabelPlacement } from '../bar/label-plan';
// ---------------------------------------------------------------------------
// Constants
@@ -63,7 +65,15 @@ export function computeColumnLabels(
darkMode = false,
fontSize?: number,
labelSuffix?: string,
+ fontFamily = 'system-ui, -apple-system, sans-serif',
+ options: {
+ placement?: BarLabelPlacement;
+ columnWidth?: number;
+ measureText?: MeasureTextFn;
+ } = {},
): ResolvedLabel[] {
+ if (options.placement === 'suppress') return [];
+ const placement = options.placement ?? 'adaptive';
const FONT_SIZE = fontSize ?? LABEL_FONT_SIZE;
const targetMarks = filterByDensity(marks, density);
@@ -72,6 +82,7 @@ export function computeColumnLabels(
const candidates: LabelCandidate[] = [];
// Per-candidate knockout-halo flag; false for labels drawn inside a segment.
const haloFlags: boolean[] = [];
+ const horizontalShifts: number[] = [];
// Track whether each candidate fits inside its stacked segment. Non-stacked
// columns label into open space above the bar, so they always fit.
const fitsInSegment: boolean[] = [];
@@ -102,13 +113,16 @@ export function computeColumnLabels(
}
}
const numericValue = parseFloat(valuePart);
- const isNegative = Number.isFinite(numericValue) && numericValue < 0;
+ const isNegative = Number.isFinite(rawNum)
+ ? rawNum < 0
+ : Number.isFinite(numericValue) && numericValue < 0;
if (labelPrefix) valuePart = labelPrefix + valuePart;
if (labelSuffix) valuePart = valuePart + labelSuffix;
- const textWidth = estimateTextWidth(valuePart, FONT_SIZE, LABEL_FONT_WEIGHT);
- const textHeight = FONT_SIZE * 1.2;
+ const measured = options.measureText?.(valuePart, FONT_SIZE, LABEL_FONT_WEIGHT);
+ const textWidth = measured?.width ?? estimateTextWidth(valuePart, FONT_SIZE, LABEL_FONT_WEIGHT);
+ const textHeight = Math.max(FONT_SIZE * 1.2, measured?.height ?? 0);
// A stacked segment has another segment sitting directly on top of it, so
// the "float above the top edge" placement used for plain columns would
@@ -128,7 +142,8 @@ export function computeColumnLabels(
let anchorY: number;
let fill: string;
- if (isStacked) {
+ const isInside = placement === 'inside' || (isStacked && placement === 'adaptive');
+ if (isInside) {
anchorY = mark.y + mark.height / 2 - textHeight / 2;
fill = labelColor ?? pickLabelColor(getRepresentativeColor(mark.fill), darkMode);
} else {
@@ -141,22 +156,30 @@ export function computeColumnLabels(
// A label only belongs inside a stacked segment if the segment is tall
// enough to hold it; short segments (Nuclear at ~4%) would otherwise
// overflow into their neighbours.
- fitsInSegment.push(!(isStacked && textHeight > mark.height - 2 * LABEL_PADDING_Y));
+ fitsInSegment.push(
+ !(
+ isInside &&
+ (textHeight > mark.height - 2 * LABEL_PADDING_Y ||
+ (placement === 'inside' && textWidth > mark.width - 4))
+ ),
+ );
// A stacked label sits ON its own segment fill, contrast-picked against it.
// A surface-colored knockout stroke would ring every glyph, so only the
// labels floating outside the bar get the halo.
- haloFlags.push(!isStacked);
+ haloFlags.push(!isInside);
+ const horizontalShift = placement === 'adaptive' ? 0 : textWidth / 2;
+ horizontalShifts.push(horizontalShift);
candidates.push({
text: valuePart,
- anchorX,
+ anchorX: anchorX - horizontalShift,
anchorY,
width: textWidth,
height: textHeight,
priority: 'data',
style: {
- fontFamily: 'system-ui, -apple-system, sans-serif',
+ fontFamily,
fontSize: FONT_SIZE,
fontWeight: LABEL_FONT_WEIGHT,
fill,
@@ -206,7 +229,7 @@ export function computeColumnLabels(
if (unfittingIndices.has(i)) {
results.push({
text: candidates[i].text,
- x: candidates[i].anchorX,
+ x: candidates[i].anchorX + horizontalShifts[i],
y: candidates[i].anchorY + ascent,
style: candidates[i].style,
visible: false,
@@ -222,13 +245,19 @@ export function computeColumnLabels(
results.push({
...rest,
...(haloFlags[i] ? {} : { halo: false }),
+ x: r.x + horizontalShifts[i],
y: r.y + ascent,
// The connector originates at the label; shift its start to the baseline
// too so the leader stays glued to the shifted text.
connector: r.connector
? {
...r.connector,
- from: { ...r.connector.from, y: r.connector.from.y + ascent },
+ from: {
+ ...r.connector.from,
+ x: r.connector.from.x + horizontalShifts[i],
+ y: r.connector.from.y + ascent,
+ },
+ to: { ...r.connector.to, x: r.connector.to.x + horizontalShifts[i] },
}
: undefined,
});
diff --git a/packages/engine/src/charts/dot/index.ts b/packages/engine/src/charts/dot/index.ts
index c9fa4b34..50c68af6 100644
--- a/packages/engine/src/charts/dot/index.ts
+++ b/packages/engine/src/charts/dot/index.ts
@@ -42,6 +42,7 @@ export const dotRenderer: ChartRenderer = (spec, scales, chartArea, strategy, th
spec.labels.prefix,
labelFormatter,
valueField,
+ theme.fonts.family,
);
let labelIdx = 0;
for (const mark of marks) {
diff --git a/packages/engine/src/charts/dot/labels.ts b/packages/engine/src/charts/dot/labels.ts
index b7c0e33c..106cb5d3 100644
--- a/packages/engine/src/charts/dot/labels.ts
+++ b/packages/engine/src/charts/dot/labels.ts
@@ -50,6 +50,7 @@ export function computeDotLabels(
labelPrefix?: string,
labelFormatter?: NumberFormatter | null,
valueField?: string,
+ fontFamily = 'system-ui, -apple-system, sans-serif',
): ResolvedLabel[] {
const targetMarks = filterByDensity(marks, density);
@@ -92,7 +93,7 @@ export function computeDotLabels(
height: textHeight,
priority: 'data',
style: {
- fontFamily: 'system-ui, -apple-system, sans-serif',
+ fontFamily,
fontSize: LABEL_FONT_SIZE,
fontWeight: LABEL_FONT_WEIGHT,
fill: getRepresentativeColor(mark.fill),
diff --git a/packages/engine/src/charts/line/__tests__/compute.test.ts b/packages/engine/src/charts/line/__tests__/compute.test.ts
index 82130a50..f13deb91 100644
--- a/packages/engine/src/charts/line/__tests__/compute.test.ts
+++ b/packages/engine/src/charts/line/__tests__/compute.test.ts
@@ -5,7 +5,6 @@ import type { NormalizedChartSpec } from '../../../compiler/types';
import { computeScales } from '../../../layout/scales';
import { computeAreaMarks } from '../area';
import { computeLineMarks } from '../compute';
-import { computeLineLabels } from '../labels';
const DARK_THEME = adaptTheme(resolveTheme());
@@ -22,13 +21,6 @@ const fullStrategy: LayoutStrategy = {
axisLabelDensity: 'full',
};
-const compactStrategy: LayoutStrategy = {
- labelMode: 'none',
- legendPosition: 'top',
- annotationPosition: 'tooltip-only',
- axisLabelDensity: 'minimal',
-};
-
function makeSingleSeriesSpec(): NormalizedChartSpec {
return {
markType: 'line',
@@ -882,121 +874,6 @@ describe('computeAreaMarks', () => {
});
});
-// ---------------------------------------------------------------------------
-// Label computation tests
-// ---------------------------------------------------------------------------
-
-describe('computeLineLabels', () => {
- it('produces labels for multi-series line marks', () => {
- const spec = makeMultiSeriesSpec();
- const scales = computeScales(spec, chartArea, spec.data);
- const allMarks = computeLineMarks(spec, scales, chartArea, fullStrategy);
-
- const lineMarks = allMarks.filter((m): m is LineMark => m.type === 'line');
- const labelMap = computeLineLabels(lineMarks, fullStrategy);
-
- expect(labelMap.size).toBe(2);
- expect(labelMap.has('US')).toBe(true);
- expect(labelMap.has('UK')).toBe(true);
- });
-
- it('labels are positioned at end of line', () => {
- const spec = makeMultiSeriesSpec();
- const scales = computeScales(spec, chartArea, spec.data);
- const allMarks = computeLineMarks(spec, scales, chartArea, fullStrategy);
-
- const lineMarks = allMarks.filter((m): m is LineMark => m.type === 'line');
- const labelMap = computeLineLabels(lineMarks, fullStrategy);
-
- for (const lineMark of lineMarks) {
- if (!lineMark.seriesKey) continue;
- const lastPoint = lineMark.points[lineMark.points.length - 1];
- const label = labelMap.get(lineMark.seriesKey);
- expect(label).toBeDefined();
- // Label x should be to the right of the last point
- expect(label!.x).toBeGreaterThan(lastPoint.x);
- }
- });
-
- it('labels are visible at full width', () => {
- const spec = makeMultiSeriesSpec();
- const scales = computeScales(spec, chartArea, spec.data);
- const allMarks = computeLineMarks(spec, scales, chartArea, fullStrategy);
-
- const lineMarks = allMarks.filter((m): m is LineMark => m.type === 'line');
- const labelMap = computeLineLabels(lineMarks, fullStrategy);
-
- // At least some labels should be visible
- const labels = Array.from(labelMap.values());
- expect(labels.some((l) => l.visible)).toBe(true);
- });
-
- it('labels collapse at compact breakpoint', () => {
- const spec = makeMultiSeriesSpec();
- const scales = computeScales(spec, chartArea, spec.data);
- const allMarks = computeLineMarks(spec, scales, chartArea, compactStrategy);
-
- const lineMarks = allMarks.filter((m): m is LineMark => m.type === 'line');
- const labelMap = computeLineLabels(lineMarks, compactStrategy);
-
- // At compact, no labels should be produced
- expect(labelMap.size).toBe(0);
- });
-
- it('collision detection resolves overlapping labels', () => {
- // Create a spec where series end at the same y position
- const spec: NormalizedChartSpec = {
- markType: 'line',
- markDef: { type: 'line', point: true },
- data: [
- { date: '2020-01-01', value: 10, country: 'A' },
- { date: '2021-01-01', value: 30, country: 'A' },
- { date: '2020-01-01', value: 10, country: 'B' },
- { date: '2021-01-01', value: 30, country: 'B' },
- { date: '2020-01-01', value: 10, country: 'C' },
- { date: '2021-01-01', value: 30, country: 'C' },
- ],
- encoding: {
- x: { field: 'date', type: 'temporal' },
- y: { field: 'value', type: 'quantitative' },
- color: { field: 'country', type: 'nominal' },
- },
- chrome: {},
- annotations: [],
- responsive: true,
- theme: {},
- darkMode: 'off',
- labels: { density: 'auto', format: '' },
- };
-
- const scales = computeScales(spec, chartArea, spec.data);
- const allMarks = computeLineMarks(spec, scales, chartArea, fullStrategy);
- const lineMarks = allMarks.filter((m): m is LineMark => m.type === 'line');
- const labelMap = computeLineLabels(lineMarks, fullStrategy);
-
- expect(labelMap.size).toBe(3);
-
- // At least one label should be offset or demoted due to collision
- const labels = Array.from(labelMap.values());
- const positions = labels.map((l) => `${l.x},${l.y}`);
- const uniquePositions = new Set(positions);
- // If collision worked, positions should differ even though anchor points are the same
- expect(uniquePositions.size).toBeGreaterThanOrEqual(2);
- });
-
- it('skips single series with no seriesKey', () => {
- const spec = makeSingleSeriesSpec();
- const scales = computeScales(spec, chartArea, spec.data);
- const allMarks = computeLineMarks(spec, scales, chartArea, fullStrategy);
-
- const lineMarks = allMarks.filter((m): m is LineMark => m.type === 'line');
- const labelMap = computeLineLabels(lineMarks, fullStrategy);
-
- // Single series has no seriesKey, so no label
- expect(labelMap.size).toBe(0);
- });
-});
-
// ---------------------------------------------------------------------------
// seriesStyles tests
// ---------------------------------------------------------------------------
diff --git a/packages/engine/src/charts/line/__tests__/density.test.ts b/packages/engine/src/charts/line/__tests__/density.test.ts
index 5d79701c..30fb5094 100644
--- a/packages/engine/src/charts/line/__tests__/density.test.ts
+++ b/packages/engine/src/charts/line/__tests__/density.test.ts
@@ -90,7 +90,7 @@ describe('mark: density', () => {
y: { axis: { title: 'Density' } },
},
} as unknown as ChartSpec);
- expect(layout.axes.y).toBeDefined();
+ expect(layout.axes.y!.label).toBe('Density');
});
it('rises monotonically when cumulative', () => {
diff --git a/packages/engine/src/charts/line/__tests__/labels.test.ts b/packages/engine/src/charts/line/__tests__/labels.test.ts
deleted file mode 100644
index 2ccb8ba4..00000000
--- a/packages/engine/src/charts/line/__tests__/labels.test.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-import type { LayoutStrategy, LineMark } from '@opendata-ai/openchart-core';
-import { describe, expect, it } from 'vitest';
-import { computeLineLabels } from '../labels';
-
-// ---------------------------------------------------------------------------
-// Fixtures
-// ---------------------------------------------------------------------------
-
-const fullStrategy: LayoutStrategy = {
- labelMode: 'all',
- legendPosition: 'right',
- annotationPosition: 'inline',
- axisLabelDensity: 'full',
-};
-
-const compactStrategy: LayoutStrategy = {
- labelMode: 'none',
- legendPosition: 'top',
- annotationPosition: 'tooltip-only',
- axisLabelDensity: 'minimal',
-};
-
-function makeLine(series: string, color: string, yOffset: number): LineMark {
- return {
- markType: 'line',
- markDef: { type: 'line' },
- points: [
- { x: 50, y: 100 + yOffset },
- { x: 150, y: 80 + yOffset },
- { x: 250, y: 120 + yOffset },
- { x: 350, y: 60 + yOffset },
- ],
- stroke: color,
- strokeWidth: 2,
- seriesKey: series,
- data: [],
- aria: { label: `${series} trend line` },
- };
-}
-
-const marks: LineMark[] = [
- makeLine('US', '#4e79a7', 0),
- makeLine('UK', '#f28e2c', 30),
- makeLine('Canada', '#e15759', 60),
-];
-
-// ---------------------------------------------------------------------------
-// Tests
-// ---------------------------------------------------------------------------
-
-describe('computeLineLabels density modes', () => {
- it('density "auto" produces labels with collision detection', () => {
- const labelMap = computeLineLabels(marks, fullStrategy, 'auto');
- expect(labelMap.size).toBeGreaterThan(0);
- // Each label should have a visibility flag
- for (const label of labelMap.values()) {
- expect(typeof label.visible).toBe('boolean');
- }
- });
-
- it('density "all" shows every series label as visible', () => {
- const labelMap = computeLineLabels(marks, fullStrategy, 'all');
- expect(labelMap.size).toBe(3);
- expect(labelMap.has('US')).toBe(true);
- expect(labelMap.has('UK')).toBe(true);
- expect(labelMap.has('Canada')).toBe(true);
- for (const label of labelMap.values()) {
- expect(label.visible).toBe(true);
- }
- });
-
- it('density "none" returns empty map', () => {
- const labelMap = computeLineLabels(marks, fullStrategy, 'none');
- expect(labelMap.size).toBe(0);
- });
-
- it('density "endpoints" works like auto for line charts', () => {
- const autoMap = computeLineLabels(marks, fullStrategy, 'auto');
- const endpointsMap = computeLineLabels(marks, fullStrategy, 'endpoints');
- // Line labels are already endpoint labels (end-of-line), so same behavior
- expect(endpointsMap.size).toBe(autoMap.size);
- });
-
- it('compact strategy suppresses labels regardless of density', () => {
- const labelMap = computeLineLabels(marks, compactStrategy, 'all');
- expect(labelMap.size).toBe(0);
- });
-
- it('default density is "auto"', () => {
- const withAuto = computeLineLabels(marks, fullStrategy, 'auto');
- const withDefault = computeLineLabels(marks, fullStrategy);
- expect(withDefault.size).toBe(withAuto.size);
- });
-});
diff --git a/packages/engine/src/charts/line/__tests__/mark-options.test.ts b/packages/engine/src/charts/line/__tests__/mark-options.test.ts
index d49d06fe..5b41e13d 100644
--- a/packages/engine/src/charts/line/__tests__/mark-options.test.ts
+++ b/packages/engine/src/charts/line/__tests__/mark-options.test.ts
@@ -117,9 +117,8 @@ describe('LineMark.dataPoints', () => {
for (const dp of lineMark.dataPoints!) {
expect(typeof dp.x).toBe('number');
expect(typeof dp.y).toBe('number');
- expect(dp.datum).toBeDefined();
- expect(dp.datum.date).toBeDefined();
- expect(dp.datum.value).toBeDefined();
+ // The original row, untouched.
+ expect(spec.data).toContainEqual(dp.datum);
}
});
});
diff --git a/packages/engine/src/charts/line/area.ts b/packages/engine/src/charts/line/area.ts
index 91a780b7..17aa900b 100644
--- a/packages/engine/src/charts/line/area.ts
+++ b/packages/engine/src/charts/line/area.ts
@@ -20,18 +20,10 @@ import type {
} from '@opendata-ai/openchart-core';
import { getRepresentativeColor, isGradientDef } from '@opendata-ai/openchart-core';
import type { ScaleLinear } from 'd3-scale';
-import {
- area,
- line,
- stack,
- stackOffsetExpand,
- stackOffsetNone,
- stackOffsetSilhouette,
- stackOrderNone,
-} from 'd3-shape';
+import { area, line } from 'd3-shape';
import { dedupeKeys, serializeKeyValue } from '../../compiler/keys';
import type { NormalizedChartSpec } from '../../compiler/types';
-import { finiteNumber } from '../../data/finite-number';
+import { computeAreaStack } from '../../data/area-stack';
import type { ResolvedScales } from '../../layout/scales';
import { getColor, resolveSeriesStroke, scaleValue, sortByField } from '../utils';
import { LINE_STROKE_WIDTH } from './compute';
@@ -284,77 +276,16 @@ function computeStackedArea(
return computeSingleArea(spec, scales, chartArea, theme);
}
- // Sort data by x field so stacked areas render left-to-right.
- // For nominal/ordinal axes, preserve data order.
- const sortedData =
- xChannel.type === 'nominal' || xChannel.type === 'ordinal'
- ? spec.data
- : sortByField(spec.data, xChannel.field);
-
- // Collect unique series keys and x values, and build a lookup from
- // (x-value, series-key) -> original data row so stacked area marks
- // get original rows instead of pivot rows.
- const seriesKeys = new Set();
- const xValueSet = new Set();
- const rowsByXSeries = new Map();
- const rowsByX = new Map();
-
- for (const row of sortedData) {
- const xStr = String(row[xChannel.field]);
- const series = String(row[colorField]);
- seriesKeys.add(series);
- xValueSet.add(xStr);
- rowsByXSeries.set(`${xStr}::${series}`, row);
-
- const existing = rowsByX.get(xStr);
- if (existing) {
- existing.push(row);
- } else {
- rowsByX.set(xStr, [row]);
- }
- }
-
- const keys = Array.from(seriesKeys);
- const xValues = Array.from(xValueSet);
-
- // An explicitly missing observation makes the cumulative boundary unknown.
- // Keep a shared gap at that x rather than inventing a zero-width segment.
- const missingBoundaries = new Set();
- // Build a pivot table: one row per x value, one column per series
- const pivotData: Record[] = xValues.map((xVal) => {
- const pivot: Record = { __x__: xVal };
- for (const key of keys) {
- pivot[key] = 0;
- }
- // Fill in actual values from pre-grouped data
- const xRows = rowsByX.get(xVal);
- if (xRows) {
- for (const row of xRows) {
- const series = String(row[colorField]);
- const value = finiteNumber(row[yChannel.field]);
- if (value === undefined) missingBoundaries.add(xVal);
- pivot[series] = value ?? 0;
- }
- }
- return pivot;
- });
-
- // Resolve stack offset from the y channel's stack property
const stackProp = yChannel.stack;
- const offsetFn =
- stackProp === 'normalize'
- ? stackOffsetExpand
- : stackProp === 'center'
- ? stackOffsetSilhouette
- : stackOffsetNone;
-
- // Use d3 stack to compute the stacked layout
- const stackGenerator = stack>()
- .keys(keys)
- .order(stackOrderNone)
- .offset(offsetFn);
-
- const stackedData = stackGenerator(pivotData);
+ const { stackedData, rowsByXSeries, missingBoundaries } = computeAreaStack(
+ spec.data,
+ xChannel.field,
+ yChannel.field,
+ colorField,
+ xChannel.type,
+ yChannel.stack,
+ spec.areaStackCache,
+ );
const yScale = scales.y.scale as ScaleLinear;
const marks: AreaMark[] = [];
@@ -368,6 +299,8 @@ function computeStackedArea(
yBottom: number;
xKey: string;
datum: DataRow;
+ observed: boolean;
+ segment: number;
}[] = [];
const segments: (typeof validPoints)[] = [[]];
@@ -388,7 +321,15 @@ function computeStackedArea(
}
const datum = rowsByXSeries.get(`${xKey}::${seriesKey}`) ?? d.data;
- const point = { x: xVal, yTop, yBottom, xKey, datum };
+ const point = {
+ x: xVal,
+ yTop,
+ yBottom,
+ xKey,
+ datum,
+ observed: rowsByXSeries.has(`${xKey}::${seriesKey}`),
+ segment: segments.length - 1,
+ };
validPoints.push(point);
segments[segments.length - 1].push(point);
}
@@ -454,7 +395,14 @@ function computeStackedArea(
stacked: true,
stackNormalized: stackProp === 'normalize',
data: validPoints.map((point) => point.datum),
- dataPoints: validPoints.map((point) => ({ x: point.x, y: point.yTop, datum: point.datum })),
+ dataPoints: validPoints.map((point) => ({
+ x: point.x,
+ y: point.yTop,
+ yBottom: point.yBottom,
+ datum: point.datum,
+ observed: point.observed,
+ segment: point.segment,
+ })),
aria,
});
}
diff --git a/packages/engine/src/charts/line/compute.ts b/packages/engine/src/charts/line/compute.ts
index 8b39c600..8234ccfd 100644
--- a/packages/engine/src/charts/line/compute.ts
+++ b/packages/engine/src/charts/line/compute.ts
@@ -54,6 +54,24 @@ const ENDPOINT_DOT_RADIUS = 4;
* renders solid so single-value charts keep the default look. */
const STROKE_DASH_PATTERNS = ['', '6 4', '2 3', '8 4 2 4', '4 4', '1 3'];
+/**
+ * Dash pattern per strokeDash value, in explicit `scale.domain` order, then
+ * first-seen data order. Shared with the legend so swatches match the lines.
+ */
+export function strokeDashPatterns(spec: NormalizedChartSpec): Map {
+ const patterns = new Map();
+ const dash = spec.encoding.strokeDash;
+ if (!dash || !('field' in dash) || !dash.field) return patterns;
+ const domain = (dash.scale?.domain as unknown[] | undefined) ?? [];
+ for (const value of [...domain, ...spec.data.map((row) => row[dash.field])]) {
+ if (value == null) continue;
+ const key = String(value);
+ if (!patterns.has(key))
+ patterns.set(key, STROKE_DASH_PATTERNS[patterns.size % STROKE_DASH_PATTERNS.length]);
+ }
+ return patterns;
+}
+
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
@@ -92,15 +110,7 @@ export function computeLineMarks(
const dashEnc =
encoding.strokeDash && 'field' in encoding.strokeDash ? encoding.strokeDash : undefined;
const dashField = dashEnc?.field;
- const dashIndexByValue = new Map();
- if (dashField) {
- for (const row of spec.data) {
- const value = row[dashField];
- if (value == null) continue;
- const key = String(value);
- if (!dashIndexByValue.has(key)) dashIndexByValue.set(key, dashIndexByValue.size);
- }
- }
+ const dashPatterns = strokeDashPatterns(spec);
const groups = groupByField(spec.data, colorField ?? dashField);
// Point separator stroke: the classic white halo reads as a bright ring grid
// on a dark canvas, so match the resolved background when it is opaque
@@ -230,11 +240,8 @@ export function computeLineMarks(
else if (styleOverride?.lineStyle === 'dotted') strokeDasharray = '2 3';
else if (dashField) {
const dashValue = rows[0]?.[dashField];
- if (dashValue != null) {
- const idx = dashIndexByValue.get(String(dashValue)) ?? 0;
- const pattern = STROKE_DASH_PATTERNS[idx % STROKE_DASH_PATTERNS.length];
- if (pattern) strokeDasharray = pattern;
- }
+ const pattern = dashValue == null ? undefined : dashPatterns.get(String(dashValue));
+ if (pattern) strokeDasharray = pattern;
}
// Create the LineMark with the combined path points.
diff --git a/packages/engine/src/charts/line/curves.ts b/packages/engine/src/charts/line/curves.ts
index 55a21dba..d1cd18cd 100644
--- a/packages/engine/src/charts/line/curves.ts
+++ b/packages/engine/src/charts/line/curves.ts
@@ -1,3 +1,4 @@
+import type { NormalizedMarkDef } from '../../compiler/types';
/**
* Curve interpolation mapping and path builders.
*
@@ -9,7 +10,7 @@
* SVG paths from point arrays without pulling in chart-specific code.
*/
-import type { MarkDef, Point } from '@opendata-ai/openchart-core';
+import type { Point } from '@opendata-ai/openchart-core';
import type { CurveFactory } from 'd3-shape';
import {
area,
@@ -25,7 +26,7 @@ import {
} from 'd3-shape';
/** Map of interpolation string names to d3 curve factories. */
-const CURVE_MAP: Record, CurveFactory> = {
+const CURVE_MAP: Record, CurveFactory> = {
linear: curveLinear,
monotone: curveMonotoneX,
step: curveStep,
@@ -40,7 +41,7 @@ const CURVE_MAP: Record, CurveFactory> = {
* Resolve an interpolation string to a d3 curve factory.
* Defaults to `curveMonotoneX` when no interpolation is specified.
*/
-export function resolveCurve(interpolate?: MarkDef['interpolate']): CurveFactory {
+export function resolveCurve(interpolate?: NormalizedMarkDef['interpolate']): CurveFactory {
if (!interpolate) return curveMonotoneX;
return CURVE_MAP[interpolate] ?? curveMonotoneX;
}
@@ -51,7 +52,7 @@ export function resolveCurve(interpolate?: MarkDef['interpolate']): CurveFactory
*/
export function buildLinePath(points: Point[], interpolate?: string): string {
if (points.length === 0) return '';
- const curve = resolveCurve(interpolate as MarkDef['interpolate']);
+ const curve = resolveCurve(interpolate as NormalizedMarkDef['interpolate']);
const generator = line()
.x((d) => d.x)
.y((d) => d.y)
@@ -69,7 +70,7 @@ export function buildAreaPath(
interpolate?: string,
): string {
if (topPoints.length === 0) return '';
- const curve = resolveCurve(interpolate as MarkDef['interpolate']);
+ const curve = resolveCurve(interpolate as NormalizedMarkDef['interpolate']);
// Pair top and bottom points for the area generator
const paired = topPoints.map((tp, i) => ({
x: tp.x,
diff --git a/packages/engine/src/charts/line/index.ts b/packages/engine/src/charts/line/index.ts
index 9bec2287..00751774 100644
--- a/packages/engine/src/charts/line/index.ts
+++ b/packages/engine/src/charts/line/index.ts
@@ -17,7 +17,6 @@ import type { ChartRenderer } from '../registry';
import { resolveKnockoutColor } from '../utils';
import { computeAreaMarks } from './area';
import { computeLineMarks } from './compute';
-import { computeLineLabels } from './labels';
/** Radius for area-chart data points, matching the line renderer. */
const AREA_POINT_RADIUS = 3;
@@ -29,34 +28,12 @@ const AREA_POINT_RADIUS = 3;
/**
* Line chart renderer.
*
- * Computes line marks + point marks for hover targets, then resolves
- * end-of-line labels and attaches them to the corresponding line marks.
+ * Computes line marks + point marks for hover targets. Series names come from
+ * the endpoint column or legend, not from line-end labels.
*/
export const lineRenderer: ChartRenderer = (spec, scales, chartArea, strategy, theme) => {
const marks = computeLineMarks(spec, scales, chartArea, strategy, theme);
- // Extract just the line marks for label computation
- const lineMarks = marks.filter((m): m is LineMark => m.type === 'line');
-
- // Compute and attach labels to line marks by seriesKey lookup. Passing the
- // spec engages the shared suppression truth table so end-of-line labels
- // hide when the legend or endpoint column is showing.
- const labelMap = computeLineLabels(
- lineMarks,
- strategy,
- spec.labels.density,
- spec.labels.offsets,
- spec,
- );
- for (const mark of marks) {
- if (mark.type === 'line' && mark.seriesKey) {
- const label = labelMap.get(mark.seriesKey);
- if (label) {
- mark.label = label;
- }
- }
- }
-
return marks as Mark[];
};
@@ -181,4 +158,3 @@ function pointsFromAreas(
export { computeAreaMarks } from './area';
export { computeLineMarks } from './compute';
-export { computeLineLabels } from './labels';
diff --git a/packages/engine/src/charts/line/labels.ts b/packages/engine/src/charts/line/labels.ts
deleted file mode 100644
index 29596988..00000000
--- a/packages/engine/src/charts/line/labels.ts
+++ /dev/null
@@ -1,177 +0,0 @@
-/**
- * Line chart label computation.
- *
- * Produces end-of-line labels (series name at the last data point)
- * and feeds them through the core collision engine. At compact
- * breakpoints, labels are suppressed in favor of the legend.
- *
- * Respects the spec's label density setting:
- * - 'all': show every label, skip collision detection
- * - 'auto': existing behavior (collision detection)
- * - 'endpoints': first and last data point labels per series
- * - 'none': return empty map
- */
-
-import type {
- LabelCandidate,
- LabelDensity,
- LayoutStrategy,
- LineMark,
- ResolvedLabel,
-} from '@opendata-ai/openchart-core';
-import {
- EXTENDED_OFFSET_STRATEGIES,
- estimateTextWidth,
- resolveCollisions,
-} from '@opendata-ai/openchart-core';
-
-import type { NormalizedChartSpec } from '../../compiler/types';
-import { countColorSeries, resolveSuppression } from '../../legend/suppression';
-
-// ---------------------------------------------------------------------------
-// Constants
-// ---------------------------------------------------------------------------
-
-/** Default label font size. */
-const LABEL_FONT_SIZE = 11;
-
-/** Default label font weight. */
-const LABEL_FONT_WEIGHT = 600;
-
-/** Horizontal offset from last point to label. */
-const LABEL_OFFSET_X = 6;
-
-// ---------------------------------------------------------------------------
-// Public API
-// ---------------------------------------------------------------------------
-
-/**
- * Compute end-of-line labels for line marks.
- *
- * For each series, places a label at the position of the last data point.
- * At compact breakpoints (labelMode === 'none'), all labels are hidden
- * so the legend takes over. Labels go through collision detection to
- * avoid overlap.
- *
- * Returns a Map keyed by seriesKey so callers can look up labels by
- * mark identity instead of relying on positional indices.
- *
- * @param marks - Line marks (only processes marks with type === 'line').
- * @param strategy - Layout strategy from the responsive breakpoint.
- * @param density - Label density mode from spec.labels.density.
- * @param labelOffsets - Per-series user offsets applied after collision resolution.
- * @param spec - Optional normalized spec. When provided, the shared suppression
- * truth table gates rendering: end-of-line labels are the last-resort series
- * identifier and only render when both the traditional legend and endpoint
- * labels column are off.
- * @returns Map of seriesKey -> ResolvedLabel after collision detection.
- */
-export function computeLineLabels(
- marks: LineMark[],
- strategy: LayoutStrategy,
- density: LabelDensity = 'auto',
- labelOffsets?: Record,
- spec?: NormalizedChartSpec,
-): Map {
- const result = new Map();
-
- // 'none': no labels
- if (density === 'none') return result;
-
- // At compact breakpoint, suppress inline labels entirely
- if (strategy.labelMode === 'none') {
- return result;
- }
-
- // Suppression truth table: when the spec is available, defer to the shared
- // helper. End-of-line labels render only when both the legend and endpoint
- // column are off — they're the last-resort series identifier.
- if (spec) {
- // The 'none' branches above already returned, so by here labelMode and
- // density are not 'none'. Pass false explicitly to keep the helper pure.
- const suppression = resolveSuppression(spec, {
- seriesCount: countColorSeries(spec),
- labelsHiddenByStrategy: false,
- labelsDensityNone: false,
- });
- if (!suppression.showEndOfLineLabels) {
- // Single-series charts have no series-name end-of-line label to hide
- // (computeLineLabels returns nothing when seriesKey is empty). For
- // multi-series, the suppression result tells us to skip explicitly.
- const isMultiSeries = !!spec.encoding.color && countColorSeries(spec) >= 2;
- if (isMultiSeries) return result;
- }
- }
-
- const candidates: LabelCandidate[] = [];
- const seriesOrder: string[] = [];
-
- for (const mark of marks) {
- if (mark.points.length === 0) continue;
-
- const labelText = mark.seriesKey ?? '';
- if (!labelText) continue;
-
- const lastPoint = mark.points[mark.points.length - 1];
- const textWidth = estimateTextWidth(labelText, LABEL_FONT_SIZE, LABEL_FONT_WEIGHT);
- const textHeight = LABEL_FONT_SIZE * 1.2;
-
- candidates.push({
- text: labelText,
- anchorX: lastPoint.x + LABEL_OFFSET_X,
- anchorY: lastPoint.y - textHeight / 2,
- width: textWidth,
- height: textHeight,
- priority: 'data',
- style: {
- fontFamily: 'system-ui, -apple-system, sans-serif',
- fontSize: LABEL_FONT_SIZE,
- fontWeight: LABEL_FONT_WEIGHT,
- fill: mark.stroke,
- lineHeight: 1.2,
- textAnchor: 'start',
- dominantBaseline: 'central',
- },
- });
-
- seriesOrder.push(labelText);
- }
-
- if (candidates.length === 0) return result;
-
- // 'all': skip collision detection, mark everything visible
- if (density === 'all') {
- for (let i = 0; i < candidates.length; i++) {
- const c = candidates[i];
- const seriesKey = seriesOrder[i];
- const userOffset = labelOffsets?.[seriesKey];
- result.set(seriesKey, {
- text: c.text,
- x: c.anchorX + (userOffset?.dx ?? 0),
- y: c.anchorY + (userOffset?.dy ?? 0),
- style: c.style,
- visible: true,
- });
- }
- return result;
- }
-
- // 'endpoints': for line charts, endpoints means showing just the end-of-line
- // label (which is what we already compute). This is the same as 'auto' for lines
- // since we only compute the endpoint label per series.
-
- const resolved = resolveCollisions(candidates, EXTENDED_OFFSET_STRATEGIES);
- for (let i = 0; i < resolved.length; i++) {
- const seriesKey = seriesOrder[i];
- const label = resolved[i];
- // Apply user-provided per-series label offset after collision resolution
- const userOffset = labelOffsets?.[seriesKey];
- if (userOffset) {
- label.x += userOffset.dx ?? 0;
- label.y += userOffset.dy ?? 0;
- }
- result.set(seriesKey, label);
- }
-
- return result;
-}
diff --git a/packages/engine/src/charts/parliament/__tests__/compute.test.ts b/packages/engine/src/charts/parliament/__tests__/compute.test.ts
index 1929ce55..41c8e31d 100644
--- a/packages/engine/src/charts/parliament/__tests__/compute.test.ts
+++ b/packages/engine/src/charts/parliament/__tests__/compute.test.ts
@@ -146,8 +146,9 @@ describe('parliament majority line', () => {
const layout = compileHouse();
const rule = layout.marks.find((m): m is RuleMarkLayout => m.type === 'rule');
const label = layout.marks.find((m): m is TextMarkLayout => m.type === 'textMark');
- expect(rule).toBeDefined();
- expect(label).toBeDefined();
+ // A vertical rule through the hemicycle's center at the simple majority.
+ expect(rule!.x1).toBe(rule!.x2);
+ expect(rule!.data).toEqual({ majority: 218 });
expect(label?.text).toBe('218 to win');
// The label sits at `lineTop - 6`, which assumes the SVG default (alphabetic)
// baseline. Stamping a dominantBaseline would shift it off the rule.
diff --git a/packages/engine/src/charts/pie/__tests__/compute.test.ts b/packages/engine/src/charts/pie/__tests__/compute.test.ts
index d1c8b411..e1cc4204 100644
--- a/packages/engine/src/charts/pie/__tests__/compute.test.ts
+++ b/packages/engine/src/charts/pie/__tests__/compute.test.ts
@@ -109,8 +109,7 @@ describe('computePieMarks', () => {
const marks = computePieMarks(spec, scales, chartArea, fullStrategy, false);
for (const mark of marks) {
- expect(mark.path).toBeTruthy();
- expect(mark.path.length).toBeGreaterThan(0);
+ expect(mark.path).toMatch(/^M[-\d.]/);
}
});
@@ -179,9 +178,10 @@ describe('computePieMarks', () => {
const marks = computePieMarks(spec, scales, chartArea, fullStrategy, false);
for (const mark of marks) {
- expect(mark.centroid).toBeDefined();
- expect(typeof mark.centroid.x).toBe('number');
- expect(typeof mark.centroid.y).toBe('number');
+ // The centroid sits inside the slice: within outerRadius of the pie center.
+ const dist = Math.hypot(mark.centroid.x - mark.center.x, mark.centroid.y - mark.center.y);
+ expect(dist).toBeGreaterThan(0);
+ expect(dist).toBeLessThan(mark.outerRadius);
}
});
});
@@ -291,7 +291,7 @@ describe('computePieMarks', () => {
const marks = computePieMarks(spec, scales, chartArea, fullStrategy, true);
for (const mark of marks) {
- expect(mark.path).toBeTruthy();
+ expect(mark.path).toMatch(/^M[-\d.]/);
expect(mark.outerRadius).toBeGreaterThan(0);
expect(mark.innerRadius).toBeGreaterThan(0);
}
@@ -391,8 +391,11 @@ describe('computePieLabels', () => {
const labels = computePieLabels(marks, chartArea);
const visibleLabels = labels.filter((l) => l.visible);
+ expect(visibleLabels.length).toBeGreaterThan(0);
for (const label of visibleLabels) {
- expect(label.connector).toBeDefined();
+ // Runs from the label to its own slice's centroid.
+ expect(label.connector!.from).toEqual({ x: label.x, y: label.y });
+ expect(label.connector!.to).toEqual(marks[label.index!].centroid);
}
});
});
diff --git a/packages/engine/src/charts/pie/__tests__/labels.test.ts b/packages/engine/src/charts/pie/__tests__/labels.test.ts
index 0497c3e9..4ffd7630 100644
--- a/packages/engine/src/charts/pie/__tests__/labels.test.ts
+++ b/packages/engine/src/charts/pie/__tests__/labels.test.ts
@@ -127,8 +127,9 @@ describe('computePieLabels positioning', () => {
for (const label of visibleLabels) {
expect(label.connector).toBeDefined();
expect(label.connector!.from).toEqual({ x: label.x, y: label.y });
- expect(label.connector!.to).toBeDefined();
- expect(label.connector!.stroke).toBeDefined();
+ // Ends on its own slice's centroid, in the default text color.
+ expect(label.connector!.to).toEqual(marks[label.index!].centroid);
+ expect(label.connector!.stroke).toBe('#333333');
}
});
diff --git a/packages/engine/src/charts/post-process.ts b/packages/engine/src/charts/post-process.ts
index ec656a1c..4ae8c314 100644
--- a/packages/engine/src/charts/post-process.ts
+++ b/packages/engine/src/charts/post-process.ts
@@ -1,3 +1,4 @@
+import type { NormalizedMarkDef } from '../compiler/types';
/**
* Mark post-processing: obstacle computation, renderer dispatch, and
* animation index assignment. These are mark-type-aware operations that
@@ -9,7 +10,6 @@ import type {
Encoding,
LineMark,
Mark,
- MarkDef,
PointMark,
RectMark,
ResolvedAnimation,
@@ -154,7 +154,7 @@ export function isBinnedBarEncoding(markType: string, encoding: Partial,
- markDef: Partial,
+ markDef: Partial,
): string {
if (markType === 'bar') {
if (isBinnedBarEncoding(markType, encoding)) {
diff --git a/packages/engine/src/charts/range/__tests__/compute.test.ts b/packages/engine/src/charts/range/__tests__/compute.test.ts
index dba60fd2..2e1e79c7 100644
--- a/packages/engine/src/charts/range/__tests__/compute.test.ts
+++ b/packages/engine/src/charts/range/__tests__/compute.test.ts
@@ -422,7 +422,7 @@ describe('compileChart with range marks', () => {
const ruleIndex = layout.marks.findIndex(
(m) => m.type === 'rule' && (m as RuleMarkLayout).key === 'Japan|range',
);
- expect(layout.tooltipDescriptors.get(`rule-${ruleIndex}`)).toBeDefined();
+ expect(layout.tooltipDescriptors.get(`rule-${ruleIndex}`)).toEqual(content);
});
it('compiles the vertical form with y/y2 and nominal x', () => {
diff --git a/packages/engine/src/charts/range/index.ts b/packages/engine/src/charts/range/index.ts
index bfe6cae9..a0426caf 100644
--- a/packages/engine/src/charts/range/index.ts
+++ b/packages/engine/src/charts/range/index.ts
@@ -58,6 +58,7 @@ export const rangeRenderer: ChartRenderer = (spec, scales, chartArea, strategy,
labelFormatter,
startField,
endField,
+ theme.fonts.family,
);
// Attach labels back onto their dots via the index carried through the
diff --git a/packages/engine/src/charts/range/labels.ts b/packages/engine/src/charts/range/labels.ts
index 373ba53f..486deee3 100644
--- a/packages/engine/src/charts/range/labels.ts
+++ b/packages/engine/src/charts/range/labels.ts
@@ -65,6 +65,7 @@ function buildCandidate(
horizontal: boolean,
outerSide: 'min' | 'max',
index: number,
+ fontFamily: string,
): LabelCandidate {
const textWidth = estimateTextWidth(text, LABEL_FONT_SIZE, LABEL_FONT_WEIGHT);
const textHeight = LABEL_FONT_SIZE * 1.2;
@@ -96,7 +97,7 @@ function buildCandidate(
priority: 'data',
index,
style: {
- fontFamily: 'system-ui, -apple-system, sans-serif',
+ fontFamily,
fontSize: LABEL_FONT_SIZE,
fontWeight: LABEL_FONT_WEIGHT,
fill: getRepresentativeColor(mark.fill),
@@ -128,6 +129,7 @@ export function computeRangeLabels(
labelFormatter?: NumberFormatter | null,
startField?: string,
endField?: string,
+ fontFamily = 'system-ui, -apple-system, sans-serif',
): ResolvedLabel[] {
if (density === 'none' || !startField || !endField) return [];
@@ -146,8 +148,22 @@ export function computeRangeLabels(
// grows downward), so the larger cy is the lower-value dot.
const startIsMin = horizontal ? pair.start.cx <= pair.end.cx : pair.start.cy >= pair.end.cy;
candidates.push(
- buildCandidate(pair.start, startText, horizontal, startIsMin ? 'min' : 'max', pairIndex * 2),
- buildCandidate(pair.end, endText, horizontal, startIsMin ? 'max' : 'min', pairIndex * 2 + 1),
+ buildCandidate(
+ pair.start,
+ startText,
+ horizontal,
+ startIsMin ? 'min' : 'max',
+ pairIndex * 2,
+ fontFamily,
+ ),
+ buildCandidate(
+ pair.end,
+ endText,
+ horizontal,
+ startIsMin ? 'max' : 'min',
+ pairIndex * 2 + 1,
+ fontFamily,
+ ),
);
}
diff --git a/packages/engine/src/charts/registry.ts b/packages/engine/src/charts/registry.ts
index a03ef953..f75b4e78 100644
--- a/packages/engine/src/charts/registry.ts
+++ b/packages/engine/src/charts/registry.ts
@@ -6,10 +6,17 @@
* The registry pattern decouples chart-type logic from the compile pipeline.
*/
-import type { LayoutStrategy, Mark, Rect, ResolvedTheme } from '@opendata-ai/openchart-core';
+import type {
+ LayoutStrategy,
+ Mark,
+ MeasureTextFn,
+ Rect,
+ ResolvedTheme,
+} from '@opendata-ai/openchart-core';
import type { NormalizedChartSpec } from '../compiler/types';
import type { ResolvedScales } from '../layout/scales';
+import type { BarLabelPlan } from './bar/label-plan';
// ---------------------------------------------------------------------------
// Types
@@ -30,6 +37,8 @@ export type ChartRenderer = (
strategy: LayoutStrategy,
theme: ResolvedTheme,
containerWidth: number,
+ labelPlan?: BarLabelPlan,
+ measureText?: MeasureTextFn,
) => Mark[];
// ---------------------------------------------------------------------------
diff --git a/packages/engine/src/charts/scatter/__tests__/compute.test.ts b/packages/engine/src/charts/scatter/__tests__/compute.test.ts
index c1f0f9ae..75892e25 100644
--- a/packages/engine/src/charts/scatter/__tests__/compute.test.ts
+++ b/packages/engine/src/charts/scatter/__tests__/compute.test.ts
@@ -579,7 +579,7 @@ describe('computeTrendLine', () => {
const points = computeScatterMarks(spec, scales, chartArea, fullStrategy);
const trendLine = computeTrendLine(points);
- expect(trendLine!.strokeDasharray).toBeTruthy();
+ expect(trendLine!.strokeDasharray).toMatch(/^\d+(\.\d+)? \d+(\.\d+)?$/);
});
it('returns null for fewer than 2 points', () => {
diff --git a/packages/engine/src/charts/utils.ts b/packages/engine/src/charts/utils.ts
index 309cacb9..5c0804b3 100644
--- a/packages/engine/src/charts/utils.ts
+++ b/packages/engine/src/charts/utils.ts
@@ -222,41 +222,7 @@ export function groupByField(data: DataRow[], field: string | undefined): Map {
- const aVal = a[field];
- const bVal = b[field];
-
- // Nulls last
- if (aVal == null && bVal == null) return 0;
- if (aVal == null) return 1;
- if (bVal == null) return -1;
-
- // Both numbers
- if (typeof aVal === 'number' && typeof bVal === 'number') {
- return aVal - bVal;
- }
-
- // Both Dates
- if (aVal instanceof Date && bVal instanceof Date) {
- return aVal.getTime() - bVal.getTime();
- }
-
- // String values: try numeric parse, then lexicographic
- const aStr = String(aVal);
- const bStr = String(bVal);
-
- const aNum = Number(aStr);
- const bNum = Number(bStr);
- if (Number.isFinite(aNum) && Number.isFinite(bNum)) {
- return aNum - bNum;
- }
-
- return aStr.localeCompare(bStr);
- });
-}
+export { sortByField } from '../data/sort-by-field';
// ---------------------------------------------------------------------------
// Color helpers
diff --git a/packages/engine/src/charts/waffle/__tests__/compute.test.ts b/packages/engine/src/charts/waffle/__tests__/compute.test.ts
index c3384318..7fbd3285 100644
--- a/packages/engine/src/charts/waffle/__tests__/compute.test.ts
+++ b/packages/engine/src/charts/waffle/__tests__/compute.test.ts
@@ -31,6 +31,7 @@ function makeWaffleSpec(overrides: Partial = {}): Normalize
markDef: { type: 'waffle' },
data: [...energyMix],
encoding: {
+ // Normalized renderers consume the internal theta-to-y mapping.
y: { field: 'share', type: 'quantitative' },
color: { field: 'source', type: 'nominal' },
},
@@ -320,7 +321,7 @@ describe('waffle "other" bucketing (opt-in)', () => {
mark,
data: longTail,
encoding: {
- y: { field: 'share', type: 'quantitative' },
+ theta: { field: 'share', type: 'quantitative' },
color: { field: 'source', type: 'nominal' },
},
},
@@ -379,7 +380,7 @@ describe('waffle "other" bucketing (opt-in)', () => {
{ source: 'Tidal', share: 2 },
],
encoding: {
- y: { field: 'share', type: 'quantitative' },
+ theta: { field: 'share', type: 'quantitative' },
color: { field: 'source', type: 'nominal' },
},
},
diff --git a/packages/engine/src/compile.ts b/packages/engine/src/compile.ts
index ddde00e3..9f853610 100644
--- a/packages/engine/src/compile.ts
+++ b/packages/engine/src/compile.ts
@@ -1,9 +1,7 @@
-import { mergeThemeConfig } from '@opendata-ai/openchart-core';
-import { diagnoseChart, noteCapacityDiagnostic } from './design/diagnostics';
/**
* Main compile API: the public entry points for the engine.
*
- * Pipeline for charts:
+ * Pipeline for charts (see ./compile/chart.ts for the phases):
* validate spec -> normalize -> resolve theme -> dark mode adapt ->
* filter data -> resolveLayoutPlan (measure real labels) ->
* compute dimensions (with plan) -> placeLegend ->
@@ -11,250 +9,50 @@ import { diagnoseChart, noteCapacityDiagnostic } from './design/diagnostics';
* get chart renderer -> compute marks -> compute a11y -> return ChartLayout
*
* Table compiler handles full data pipeline (sort, search, pagination, visual enhancements).
- * Graph compiler is a stub for future implementation.
*/
import type {
- AnimationSpec,
+ BarListLayout,
ChartLayout,
- ChartSpec,
CompileOptions,
CompileTableOptions,
- Encoding,
- FacetChannel,
- FacetPanelLayout,
+ DesignDiagnostic,
+ GeoMapLayout,
LayerSpec,
- LayoutStrategy,
- LegendLayout,
- Mark,
- ResolvedAnimation,
- ResolvedAnnotation,
- ResolvedTheme,
- ResolveMode,
- ScaleInvert,
+ SankeyLayout,
TableLayout,
+ TileMapLayout,
} from '@opendata-ai/openchart-core';
-import {
- adaptTheme,
- computeLabelBounds,
- computeNoteBandHeight,
- generateAltText,
- generateDataTable,
- getBreakpoint,
- getHeightClass,
- getLayoutStrategy,
- isAxislessMark,
- MAX_SR_TABLE_ROWS,
- resolveChromeEconomy,
- resolveTheme,
- wrapText,
-} from '@opendata-ai/openchart-core';
-import { collectPinnedOverlapWarnings } from './annotations/collisions';
-import { computeAnnotations } from './annotations/compute';
-import { type AnnotationMeasureTextFn, heuristicMeasure } from './annotations/geometry';
-import {
- annotationNoteCapacity,
- annotationNoteLineCount,
- wrapAnnotationNotes,
-} from './annotations/notes';
-import type { PlacementObstacle } from './annotations/placement';
-import { referenceLabelReserve } from './annotations/reference-layout';
-import { thinAnnotations } from './annotations/thinning';
-import { collectMarkOptionWarnings } from './compiler/mark-options';
-import { computeEndpointLabels } from './endpoint-labels/compute';
-import { ENDPOINT_COLUMN_GAP } from './endpoint-labels/constants';
+import { assertMarkOptions } from './compiler/mark-options';
// Side-effect import: registers all built-in chart renderers with the
// registry on module load. Tests that clear the registry can import
// `registerBuiltinRenderers` from `./charts/builtin` to restore defaults.
import './charts/builtin';
import { compileBarList as compileBarListImpl } from './barlist/compile-barlist';
-import {
- assignAnimationIndices,
- computeMarkObstacles,
- isBinnedBarEncoding,
- resolveRendererKey,
-} from './charts/post-process';
-import { getChartRenderer } from './charts/registry';
-import { groupByField } from './charts/utils';
-import { applyColorScaleRange, defaultSeriesColor } from './compile/color-scale-range';
-import { filterClippedDomains } from './compile/data-clip';
-import { applyFillPatterns } from './compile/fill-patterns';
+import { compileChartInternal, resolveCompileTheme } from './compile/chart';
import { compileLayer as compileLayerImpl } from './compile/layer';
-import { emitSpecWarnings, emitThemeWarnings, expandSpecSugar } from './compile/spec-sugar';
-import { computeWatermarkObstacle } from './compile/watermark-obstacle';
-import { resolveYouDrawIt } from './compile/you-draw-it';
-import { collectContrastWarnings } from './compiler/a11y-warnings';
-import { resolveAnimation } from './compiler/animation';
+import { applySpecSize } from './compile/prepare';
+import { emitSpecWarnings, expandSpecSugar } from './compile/spec-sugar';
import { compile as compileSpec } from './compiler/index';
-import { resolveMarkRenderMode } from './compiler/mark-render-mode';
-import {
- buildSparklineAreaGradient,
- computeTrendFromData,
- hasExplicitColor,
- trendColor,
-} from './compiler/sparkline-defaults';
-import type { NormalizedChartSpec, NormalizedTableSpec } from './compiler/types';
+import type { NormalizedTableSpec } from './compiler/types';
+import { noteCapacityDiagnostic } from './design/diagnostics';
import { compileGeoMap as compileGeoMapImpl } from './geo/compile-geo-map';
import { compileGraph as compileGraphImpl } from './graphs/compile-graph';
import type { GraphCompilation } from './graphs/types';
-import { computeAxes } from './layout/axes';
-import { computeDimensions } from './layout/dimensions';
-import { computeFacetGrid, facetMinHeight, MIN_PANEL_HEIGHT } from './layout/facet';
-import { computeGridlines } from './layout/gridlines';
-import { createMeasureFn, resolveLayoutPlan } from './layout/plan';
-import { computeScales, type ResolvedScale } from './layout/scales';
-import { placeLegend } from './legend/compute';
-import { placeSizeLegend } from './legend/size';
+import { isNoteOverflowDiagnostic } from './layout/budget';
import { compileSankey as compileSankeyImpl } from './sankey/compile-sankey';
import { compileTableLayout } from './tables/compile-table';
import { compileTileMap as compileTileMapImpl } from './tilemap/compile-tilemap';
-import { computeTooltipDescriptors } from './tooltips/compute';
-import { runTransforms } from './transforms';
-
-// ---------------------------------------------------------------------------
-// Spec sugar expansion (VL idioms, bin/timeUnit, deprecation warnings)
-// ---------------------------------------------------------------------------
-// Implementation lives in ./compile/spec-sugar; re-exported here so existing
-// deep imports (`import { expandEncodingSugar } from '.../compile'`) keep working.
+// Spec sugar implementation lives in ./compile/spec-sugar; re-exported here so
+// existing deep imports (`import { expandEncodingSugar } from '.../compile'`)
+// keep working.
export { expandEncodingSugar, expandSpecSugar } from './compile/spec-sugar';
-/**
- * Apply top-level spec `width`/`height` (VL fixed-size sugar) as overrides on
- * the compile options. Returns the options untouched when the spec carries no
- * size.
- */
-function applySpecSize(spec: unknown, options: CompileOptions): CompileOptions {
- if (!spec || typeof spec !== 'object') return options;
- const sized = spec as Record;
- if (typeof sized.width !== 'number' && typeof sized.height !== 'number') return options;
- return {
- ...options,
- width: typeof sized.width === 'number' ? sized.width : options.width,
- height:
- typeof sized.height === 'number' &&
- !(options?.facetHeightGrown && (sized.chromeLayout ?? options.chromeLayout) === 'grow')
- ? sized.height
- : options.height,
- };
-}
-
// ---------------------------------------------------------------------------
// Chart compilation
// ---------------------------------------------------------------------------
-/**
- * Cap on footnote-reserve convergence passes in compileChart. Reserving space
- * for the footnote list shrinks the plot, which can demote one more label and
- * grow the list; the count only ever grows and is bounded by the annotation
- * count, so this terminates on its own. The cap is a guard against a
- * measurement quirk oscillating, not the normal exit condition.
- */
-const MAX_FOOTNOTE_PASSES = 3;
-
-/**
- * Inject sparkline-mode visual defaults that depend on the resolved theme.
- *
- * Trend-aware color (positive/negative/neutral), default endpoint dot,
- * default area gradient, and default pill cornerRadius for bars. Each
- * default backs off when the user has set the corresponding markDef
- * field. Bar marks do NOT get trend coloring — the design intent is a
- * single palette color across all bars.
- */
-function applySparklineDefaults(
- spec: NormalizedChartSpec,
- theme: ResolvedTheme,
-): NormalizedChartSpec {
- const markType = spec.markType;
- const isLineFamily = markType === 'line' || markType === 'area';
- const isBar = markType === 'bar';
- if (!isLineFamily && !isBar) return spec;
-
- const yField = spec.encoding.y && 'field' in spec.encoding.y ? spec.encoding.y.field : undefined;
- const trend = computeTrendFromData(spec.data, yField);
- const color = trendColor(trend, theme);
-
- const encodingHasColor =
- !!spec.encoding.color && (spec.encoding.color as { field?: unknown }).field !== undefined;
- const explicit = hasExplicitColor(spec.markDef, encodingHasColor);
-
- const newMarkDef = { ...spec.markDef };
-
- if (isLineFamily) {
- if (!explicit.stroke) newMarkDef.stroke = color;
- if (markType === 'area' && !explicit.fill) {
- newMarkDef.fill = buildSparklineAreaGradient(color);
- }
- // Endpoint dot is wired in the line compute path. We skip the default
- // when the encoding has a color field, because the multi-series area
- // path (linesFromAreas()) ignores markDef.point — the dot would be set
- // on the spec but never rendered, and the line compute path's dot logic
- // is per-series, not "one dot for the chart."
- if (newMarkDef.point === undefined && !encodingHasColor) {
- newMarkDef.point = 'last';
- }
- }
-
- if (isBar) {
- // Pill cornerRadius is safe on stacked bars too — the column compute
- // path applies the radius only to the topmost segment via
- // `cornerRadiusSides`, leaving the seams between segments square so
- // they stay flush. Single-series bars get all four corners rounded
- // since there's nothing below them to align against.
- if (newMarkDef.cornerRadius === undefined) {
- newMarkDef.cornerRadius = 'pill';
- }
- }
-
- return { ...spec, markDef: newMarkDef };
-}
-
-/**
- * Extract scale inversion anchors from a resolved scale so the vanilla layer
- * can convert pixel positions back to data-space values without holding the
- * d3 scale object. Generalizes the YouDrawItYInvert precedent to any axis.
- */
-function resolveScaleInvert(resolved: ResolvedScale | undefined): ScaleInvert | undefined {
- if (!resolved) return undefined;
- const { scale, type } = resolved;
-
- // Band / point scales: snap to nearest domain value
- if (type === 'band' || type === 'point') {
- const domain = (scale as { domain(): string[] }).domain() as (string | number)[];
- const positions = domain.map((v) => {
- const pos = (scale as (v: string | number) => number | undefined)(v) ?? 0;
- return type === 'band' ? pos + (scale as { bandwidth(): number }).bandwidth() / 2 : pos;
- });
- const range = (scale as { range(): number[] }).range();
- return {
- topPixel: range[0],
- bottomPixel: range[range.length - 1],
- topData: 0,
- bottomData: 0,
- domain,
- positions,
- };
- }
-
- // Continuous scales (linear, time, log, pow, sqrt, symlog)
- const invert = (scale as { invert?: (px: number) => unknown }).invert;
- if (typeof invert !== 'function') return undefined;
-
- const range = (scale as { range(): number[] }).range();
- const topPx = range[0];
- const bottomPx = range[range.length - 1];
- const topVal = invert.call(scale, topPx);
- const bottomVal = invert.call(scale, bottomPx);
-
- // For time scales, convert Date to epoch ms
- const topData = topVal instanceof Date ? +topVal : Number(topVal);
- const bottomData = bottomVal instanceof Date ? +bottomVal : Number(bottomVal);
- if (!Number.isFinite(topData) || !Number.isFinite(bottomData)) return undefined;
-
- const scaleType = type as ScaleInvert['scaleType'];
- return { topPixel: topPx, bottomPixel: bottomPx, topData, bottomData, scaleType };
-}
-
/**
* Compile a chart spec into a ChartLayout.
*
@@ -269,1448 +67,10 @@ function resolveScaleInvert(resolved: ResolvedScale | undefined): ScaleInvert |
* @throws Error if spec is invalid or not a chart type.
*/
export function compileChart(spec: unknown, optionsInput: CompileOptions): ChartLayout {
- emitSpecWarnings(collectMarkOptionWarnings(spec), optionsInput?.onWarn);
+ assertMarkOptions(spec);
return compileChartInternal(spec, optionsInput);
}
-/** Internal passes retain authored warnings from the public entry point. */
-function compileChartInternal(spec: unknown, optionsInput: CompileOptions): ChartLayout {
- // Expand VL-idiom and encoding-level sugar (data {values}, top-level title,
- // sort-by-value, value defs, scheme names, theta, bin/timeUnit, ...) before
- // validation + normalization, surfacing any deprecation warnings.
- const sugarWarnings: string[] = [];
- const expandedSpec =
- spec && typeof spec === 'object' && !Array.isArray(spec)
- ? expandSpecSugar(spec as Record, sugarWarnings, false)
- : spec;
-
- // Top-level width/height are fixed-size overrides (VL alignment): the
- // authored size wins over the container-derived compile options.
- const options = applySpecSize(expandedSpec, optionsInput);
-
- // Validate + normalize. Normalize-stage warnings (type mismatches,
- // beeswarm point budget, sparkline mark advice) surface through the same
- // console channel as the sugar warnings. Emit both together in one call so
- // dedup spans the whole compile: a message produced by both a sugar pass and
- // a normalize pass prints once, not once per stage.
- const { spec: normalized, warnings: normalizeWarnings } = compileSpec(expandedSpec, {
- markOptionWarnings: false,
- });
- emitSpecWarnings([...sugarWarnings, ...normalizeWarnings], options.onWarn);
-
- if ('type' in normalized && (normalized as unknown as Record).type === 'table') {
- throw new Error('compileChart received a table spec. Use compileTable instead.');
- }
- if ('type' in normalized && (normalized as unknown as Record).type === 'graph') {
- throw new Error('compileChart received a graph spec. Use compileGraph instead.');
- }
- if (
- 'type' in normalized &&
- (normalized as unknown as Record).type === 'sankey'
- ) {
- throw new Error('compileChart received a sankey spec. Use compileSankey instead.');
- }
- if ('type' in normalized && (normalized as unknown as Record).type === 'map') {
- throw new Error('compileChart received a map spec. Use compileGeoMap instead.');
- }
-
- let chartSpec = normalized as NormalizedChartSpec;
-
- // Resolve watermark: explicit spec value wins, then options fallback, then default true.
- const rawWatermark = (expandedSpec as Record).watermark;
- let watermark = rawWatermark !== undefined ? chartSpec.watermark : (options.watermark ?? true);
-
- // Run data transforms (filter, bin, calculate, timeUnit) before any other data processing.
- // Transforms are defined on the expanded spec (which includes any auto-generated
- // transforms from encoding-level bin/timeUnit sugar), not the normalized spec,
- // since NormalizedChartSpec doesn't carry the transform field.
- const rawTransforms = (expandedSpec as Record).transform as
- | import('@opendata-ai/openchart-core').Transform[]
- | undefined;
- if (rawTransforms && rawTransforms.length > 0) {
- chartSpec = { ...chartSpec, data: runTransforms(chartSpec.data, rawTransforms) };
- }
-
- // Responsive strategy
- const breakpoint = getBreakpoint(options.width);
- const heightClass = getHeightClass(options.height);
- let strategy = getLayoutStrategy(breakpoint, heightClass);
- // How much chrome the container can afford: a tile drops its grid, then its
- // axes, and caps x tick labels at compact widths. Explicit axis config wins.
- const economy = resolveChromeEconomy(options.width, options.height);
-
- // Apply breakpoint-conditional overrides from the expanded spec
- const rawSpec = expandedSpec as Record;
- const overrides = rawSpec.overrides as
- | Partial<
- Record<
- string,
- {
- chrome?: unknown;
- labels?: unknown;
- legend?: unknown;
- annotations?: unknown;
- animation?: unknown;
- display?: unknown;
- encoding?: unknown;
- watermark?: unknown;
- crosshair?: unknown;
- }
- >
- >
- | undefined;
-
- // Build userExplicit descriptor BEFORE applying any overrides so we capture
- // the union of "user wrote this at top-level" and "user wrote this in the
- // active breakpoint override." Sparkline display mode reads this to decide
- // whether to suppress chrome/axes/legend/etc. by default vs. respecting an
- // explicit user opt-in. Precedence: explicit at any level wins.
- const rawEncoding = rawSpec.encoding as
- | { x?: { axis?: unknown }; y?: { axis?: unknown } }
- | undefined;
- const bpForExplicit = overrides?.[breakpoint];
- const bpEncoding = bpForExplicit?.encoding as
- | { x?: { axis?: unknown }; y?: { axis?: unknown } }
- | undefined;
- // chrome: {} (empty object) is not "explicit" — it's an idiom users write to
- // silence defaults. Require at least one chrome key set to count as opt-in.
- const hasChromeKeys = (v: unknown): boolean =>
- !!v && typeof v === 'object' && Object.keys(v as Record).length > 0;
- const userExplicit = {
- chrome: hasChromeKeys(rawSpec.chrome) || hasChromeKeys(bpForExplicit?.chrome),
- legend: rawSpec.legend !== undefined || bpForExplicit?.legend !== undefined,
- endpointLabels:
- rawSpec.endpointLabels !== undefined ||
- (bpForExplicit as Record | undefined)?.endpointLabels !== undefined,
- // Axis explicitness tracks "user opted IN to render the axis" — `axis: false`
- // is an explicit opt-OUT and must not flip these flags on. Sparkline mode
- // reads them to decide whether to reserve axis space; treating `false` as
- // explicit would leave a phantom gutter for an axis that won't render.
- xAxis:
- (rawEncoding?.x?.axis !== undefined && rawEncoding.x.axis !== false) ||
- (bpEncoding?.x?.axis !== undefined && bpEncoding.x.axis !== false),
- yAxis:
- (rawEncoding?.y?.axis !== undefined && rawEncoding.y.axis !== false) ||
- (bpEncoding?.y?.axis !== undefined && bpEncoding.y.axis !== false),
- labels: rawSpec.labels !== undefined || bpForExplicit?.labels !== undefined,
- animation: rawSpec.animation !== undefined || bpForExplicit?.animation !== undefined,
- watermark: rawSpec.watermark !== undefined || bpForExplicit?.watermark !== undefined,
- crosshair: rawSpec.crosshair !== undefined || bpForExplicit?.crosshair !== undefined,
- };
- chartSpec = { ...chartSpec, userExplicit };
-
- if (overrides?.[breakpoint]) {
- const bp = overrides[breakpoint]!;
- if (bp.chrome) {
- chartSpec = {
- ...chartSpec,
- chrome: {
- ...chartSpec.chrome,
- ...(bp.chrome as NormalizedChartSpec['chrome']),
- },
- };
- }
- if (bp.labels !== undefined) {
- if (typeof bp.labels === 'boolean') {
- chartSpec = {
- ...chartSpec,
- labels: bp.labels
- ? { density: 'auto', format: '', prefix: '' }
- : { density: 'none', format: '', prefix: '' },
- };
- } else {
- chartSpec = {
- ...chartSpec,
- labels: {
- ...chartSpec.labels,
- ...(bp.labels as NormalizedChartSpec['labels']),
- },
- };
- }
- }
- if (bp.legend) {
- chartSpec = {
- ...chartSpec,
- legend: {
- ...chartSpec.legend,
- ...(bp.legend as NormalizedChartSpec['legend']),
- },
- };
- }
- if (bp.annotations) {
- chartSpec = {
- ...chartSpec,
- annotations: bp.annotations as NormalizedChartSpec['annotations'],
- };
- // User explicitly provided annotations at this breakpoint — override the
- // responsive strategy so they render inline instead of being stripped.
- strategy = { ...strategy, annotationPosition: 'inline' };
- }
- // New override branches for sparkline mode and related fields:
- if (bp.display !== undefined) {
- chartSpec = {
- ...chartSpec,
- display: bp.display as NormalizedChartSpec['display'],
- };
- }
- if (bp.encoding !== undefined) {
- // Merge encoding so a breakpoint can flip on/off encoding.x.axis or
- // encoding.y.axis (used by sparkline display mode to opt back in to
- // axes at a specific breakpoint). Channels merge per-key, and `axis`
- // and `scale` deep-merge one level so a breakpoint can set
- // `axis: { title: 'foo' }` without dropping the base spec's
- // `axis.tickCount` / `axis.format`.
- const bpEnc = bp.encoding as Record | undefined>;
- const mergedEncoding = { ...chartSpec.encoding } as Record<
- string,
- Record | undefined
- >;
- const NESTED_CHANNEL_KEYS = ['axis', 'scale'];
- for (const channel of Object.keys(bpEnc)) {
- const baseCh = mergedEncoding[channel];
- const bpCh = bpEnc[channel];
- if (bpCh && baseCh) {
- const merged: Record = { ...baseCh, ...bpCh };
- for (const key of NESTED_CHANNEL_KEYS) {
- const baseNested = baseCh[key];
- const bpNested = bpCh[key];
- if (
- baseNested &&
- bpNested &&
- typeof baseNested === 'object' &&
- typeof bpNested === 'object' &&
- !Array.isArray(baseNested) &&
- !Array.isArray(bpNested)
- ) {
- merged[key] = { ...baseNested, ...bpNested };
- }
- }
- mergedEncoding[channel] = merged;
- } else if (bpCh) {
- mergedEncoding[channel] = bpCh;
- }
- }
- chartSpec = {
- ...chartSpec,
- encoding: mergedEncoding as unknown as NormalizedChartSpec['encoding'],
- };
- }
- if (typeof bp.watermark === 'boolean') {
- // Update the resolved watermark value used downstream. ChartSpec carries
- // this in its normalized shape; the local `watermark` variable controls
- // chrome computation and rendering.
- watermark = bp.watermark;
- chartSpec = { ...chartSpec, watermark };
- }
- }
-
- // Sparkline mode: default labels off. Mark renderers draw value labels per
- // labels.density (default 'auto'), which fills tiny sparklines with text and
- // is never what you want. Explicit user labels at any level wins via
- // userExplicit.labels.
- if (chartSpec.display === 'sparkline' && !chartSpec.userExplicit.labels) {
- chartSpec = {
- ...chartSpec,
- labels: { ...chartSpec.labels, density: 'none' },
- };
- }
-
- // Resolve animation spec. Breakpoint override wins over base spec (matching
- // chrome, labels, legend, and annotation override precedence).
- // Precedence rule for sparkline mode: an explicit user animation at ANY
- // level (top-level OR breakpoint) always wins, regardless of display mode.
- // resolveAnimation handles the explicit-user value; the sparkline default-off
- // behavior is applied below when no explicit value exists.
- let rawAnimationSpec = ((overrides?.[breakpoint] as Record | undefined)
- ?.animation ?? rawSpec.animation) as AnimationSpec | undefined;
- if (rawAnimationSpec === undefined && chartSpec.display === 'sparkline') {
- // Sparkline mode: animation defaults to false. User-explicit (top OR bp)
- // already short-circuits this branch via userExplicit.animation.
- rawAnimationSpec = false;
- }
- // Sparkline mode: when animation is on but the user didn't specify duration,
- // bump to 1100ms so the line/area reveal feels paced rather than mechanical.
- // The CSS override pairs this with an expo-out easing curve. AnimationConfig
- // nests duration under `enter`, so we set it there.
- if (
- chartSpec.display === 'sparkline' &&
- rawAnimationSpec !== false &&
- rawAnimationSpec !== undefined
- ) {
- const SPARK_DURATION = 1100;
- if (rawAnimationSpec === true) {
- rawAnimationSpec = { enter: { duration: SPARK_DURATION } } as AnimationSpec;
- } else if (typeof rawAnimationSpec === 'object') {
- const cfg = rawAnimationSpec as { enter?: unknown; annotationDelay?: number };
- const enter = cfg.enter;
- if (enter === undefined || enter === true) {
- rawAnimationSpec = {
- ...cfg,
- enter: { duration: SPARK_DURATION },
- } as AnimationSpec;
- } else if (
- typeof enter === 'object' &&
- enter !== null &&
- (enter as { duration?: number }).duration === undefined
- ) {
- rawAnimationSpec = {
- ...cfg,
- enter: { ...(enter as object), duration: SPARK_DURATION },
- } as AnimationSpec;
- }
- }
- }
- const resolvedAnimation = resolveAnimation(rawAnimationSpec);
-
- // Crosshair: explicit user value at any level wins. Default is ON for line
- // and area marks in full mode (the hover guideline + per-series tooltip is
- // the standard interaction pattern for time-series charts). Sparkline mode
- // defaults off. The value is plumbed through ChartLayout so the renderer
- // doesn't need to re-inspect the raw spec.
- const rawCrosshair = (bpForExplicit?.crosshair ?? rawSpec.crosshair) as boolean | undefined;
- const isLineOrAreaMark = chartSpec.markType === 'line' || chartSpec.markType === 'area';
- const crosshairDefault = chartSpec.display === 'sparkline' ? false : isLineOrAreaMark;
- const crosshair = chartSpec.userExplicit.crosshair ? rawCrosshair === true : crosshairDefault;
-
- // Watermark default-off in sparkline mode unless user-explicit.
- if (chartSpec.display === 'sparkline' && !chartSpec.userExplicit.watermark) {
- watermark = false;
- chartSpec = { ...chartSpec, watermark: false };
- }
-
- // Watermark auto-hides in minimal and cramped containers (height < 200px):
- // there isn't room to spare for the brand band on a tile that small, even
- // though cramped (100-199px) now keeps a compact title. Explicit user
- // watermark at any level wins.
- if (
- (heightClass === 'minimal' || heightClass === 'cramped') &&
- !chartSpec.userExplicit.watermark
- ) {
- watermark = false;
- chartSpec = { ...chartSpec, watermark: false };
- }
-
- // Resolve theme: merge spec-level theme with options-level overrides
- emitThemeWarnings(options.theme, options.onWarn);
- const mergedThemeConfig = mergeThemeConfig(chartSpec.theme, options.theme);
- let theme: ResolvedTheme = resolveTheme(mergedThemeConfig);
- if (options.darkMode) {
- theme = adaptTheme(theme);
- }
-
- // Reserve from source text before placement. A drag can pin a label and change
- // the demoted set; neither the plot nor the SVG clamp may move at commit.
- // Include pinned sources because fresh compiles cannot distinguish authored
- // pins from labels pinned by editing. Facets share this source-level budget.
- const noteCapacity = chartSpec.autoThin
- ? annotationNoteCapacity(
- chartSpec.annotations,
- options.width,
- theme,
- options.measureText,
- options.width < 400 && options.height >= 200 && chartSpec.compactAnnotationLimit != null,
- )
- : 0;
- const stableNoteReserve = computeNoteBandHeight(noteCapacity, theme);
- if (!options.frozenChartArea && stableNoteReserve > (options.footnoteReserve ?? 0)) {
- return compileChartInternal(spec, {
- ...optionsInput,
- footnoteReserve: stableNoteReserve,
- });
- }
-
- // Sparkline mode: inject smart defaults that depend on the resolved theme
- // (trend-aware color, area gradient, default endpoint dot, pill cornerRadius
- // for bars). Each default backs off when the user has set the corresponding
- // field explicitly. Must run AFTER theme resolution (color depends on theme)
- // and BEFORE computeDimensions (which reads markDef.point for sparkline padding).
- if (chartSpec.display === 'sparkline') {
- chartSpec = applySparklineDefaults(chartSpec, theme);
- }
-
- // ---------------------------------------------------------------------------
- // Data filtering: move BEFORE layout plan so the plan measures real labels
- // from the filtered data set. Legend retains all series via chartSpec
- // (unfiltered); scales and marks use renderSpec (filtered).
- // ---------------------------------------------------------------------------
- let renderData = chartSpec.data;
-
- // Filter hidden series: removed from rendering but kept in legend (dimmed in the adapter)
- if (
- chartSpec.hiddenSeries.length > 0 &&
- chartSpec.encoding.color &&
- 'field' in chartSpec.encoding.color
- ) {
- const colorField = chartSpec.encoding.color.field;
- const hiddenSet = new Set(chartSpec.hiddenSeries);
- renderData = renderData.filter((row) => !hiddenSet.has(String(row[colorField])));
- }
-
- // Filter clipped scale domains: when scale.clip is true, exclude rows outside the domain
- renderData = filterClippedDomains(renderData, chartSpec.encoding);
-
- // Build a filtered spec for scales and marks, keeping all other properties intact
- let renderSpec = renderData !== chartSpec.data ? { ...chartSpec, data: renderData } : chartSpec;
-
- // Lock the color scale domain to the unfiltered series list so palette
- // assignments stay stable across legend toggles. Without this, hiding a
- // series shrinks the ordinal scale's domain and shifts every remaining
- // series down one palette index -- the visible lines would mismatch the
- // legend swatches that still represent the original assignment.
- // Skipped when the user already supplied an explicit domain.
- const colorEnc = chartSpec.encoding.color;
- if (
- chartSpec.hiddenSeries.length > 0 &&
- colorEnc &&
- 'field' in colorEnc &&
- colorEnc.type !== 'quantitative' &&
- colorEnc.scale?.domain == null
- ) {
- const colorField = colorEnc.field;
- const stableDomainSet = new Set();
- for (const row of chartSpec.data) stableDomainSet.add(String(row[colorField]));
- const stableDomain = Array.from(stableDomainSet);
- renderSpec = {
- ...renderSpec,
- encoding: {
- ...renderSpec.encoding,
- color: {
- ...colorEnc,
- scale: { ...(colorEnc.scale ?? {}), domain: stableDomain },
- },
- },
- };
- }
-
- // ---------------------------------------------------------------------------
- // Faceted compilation: desugar row/column into facet, then take faceted path
- // ---------------------------------------------------------------------------
- let facetDirection: 'row' | 'column' | 'wrap' = 'wrap';
- const rowChannel = renderSpec.encoding.row;
- const colChannel = renderSpec.encoding.column;
- if (rowChannel?.field || colChannel?.field) {
- const channel = (rowChannel?.field ? rowChannel : colChannel)!;
- facetDirection = rowChannel?.field ? 'row' : 'column';
- const desugared = { ...renderSpec.encoding } as Record;
- delete desugared.row;
- delete desugared.column;
- desugared.facet = {
- field: channel.field,
- type: channel.type,
- sort: channel.sort,
- ...(facetDirection === 'row' ? { columns: 1 } : {}),
- };
- renderSpec = { ...renderSpec, encoding: desugared as typeof renderSpec.encoding };
- const chartDesugared = { ...chartSpec.encoding } as Record;
- delete chartDesugared.row;
- delete chartDesugared.column;
- chartDesugared.facet = desugared.facet;
- chartSpec = { ...chartSpec, encoding: chartDesugared as typeof chartSpec.encoding };
- }
-
- const facetChannel = renderSpec.encoding.facet;
- if (facetChannel?.field) {
- const layout = compileFaceted(
- chartSpec,
- renderSpec,
- facetChannel,
- facetDirection,
- { ...options, onDiagnostic: undefined },
- theme,
- strategy,
- watermark,
- resolvedAnimation,
- crosshair,
- spec,
- { ...optionsInput, onDiagnostic: undefined },
- );
- const diagnostic = noteCapacityDiagnostic(layout);
- if (diagnostic) options.onDiagnostic?.(diagnostic);
- return layout;
- }
-
- // ---------------------------------------------------------------------------
- // Layout plan: measure real labels, freeze gutter + chrome + legend content
- // ---------------------------------------------------------------------------
- const measure = createMeasureFn(options.measureText);
- const plan = resolveLayoutPlan(
- chartSpec,
- renderSpec,
- options,
- theme,
- strategy,
- watermark,
- measure,
- );
-
- // Compute dimensions (accounts for chrome + legend + responsive strategy).
- // The plan provides measured leftGutter, xAxisExtent, and chrome so
- // dimensions.ts skips its guess blocks.
- const legendStub = placeLegend(
- plan.legendContent,
- { x: 0, y: 0, width: options.width, height: options.height },
- options.width,
- theme,
- plan.xAxisExtent,
- );
- const dims = computeDimensions(chartSpec, options, legendStub, theme, strategy, watermark, plan);
- // A frozen area (set by compileLayer for leaf layers) overrides the computed
- // one so all layers share the primary layout's coordinate space.
- const chartArea = options.frozenChartArea ?? dims.chartArea;
-
- // Inline y-axis labels: inset the x-scale range start by the widest tick
- // label width so data marks start to the right of the labels. The plan
- // already measured the real tick labels.
- let scaleArea = chartArea;
- if (plan.inlineYLabelInset > 0 && plan.inlineYLabelInset < chartArea.width) {
- scaleArea = {
- x: chartArea.x + plan.inlineYLabelInset,
- y: chartArea.y,
- width: chartArea.width - plan.inlineYLabelInset,
- height: chartArea.height,
- };
- }
-
- // A histogram has one bar per bin, so a number over every bar buries the
- // distribution it exists to show. The binned-bar renderer emits no value
- // labels; say so rather than letting `labels` silently do nothing.
- if (
- chartSpec.userExplicit.labels &&
- chartSpec.labels?.density !== 'none' &&
- isBinnedBarEncoding(chartSpec.markType, renderSpec.encoding)
- ) {
- emitSpecWarnings(
- [
- "[openchart] `labels` has no effect on a histogram (binned bar): a value over every bin obscures the distribution. Remove it, or use mark: 'bar' over pre-aggregated categories if you need per-bar values.",
- ],
- options.onWarn,
- );
- }
-
- // Compute scales
- const scales = computeScales(renderSpec, scaleArea, renderSpec.data);
-
- // Update color scale to use theme palette (only when user hasn't provided an explicit range).
- // When highlight is active, assign palette colors to highlighted series and mute the rest.
- emitSpecWarnings(
- applyColorScaleRange(
- scales,
- renderSpec.encoding,
- theme,
- renderSpec.highlight,
- chartSpec.markType,
- ),
- options.onWarn,
- );
-
- // INVARIANT 3 -- post-hoc defaultColor: must run AFTER computeScales since resolution needs
- // theme context. Do not move into computeScales (would require threading theme through).
- // fill wins for bar/area/arc marks; stroke wins for line marks (the stroke IS the color).
- scales.defaultColor =
- chartSpec.markDef.fill ??
- chartSpec.markDef.stroke ??
- defaultSeriesColor(theme, chartSpec.markType);
-
- // Dev-mode WCAG contrast diagnostics. Host-gated via options.dev (never an
- // env sniff; the engine is isomorphic). Advisory console.warn only.
- if (options.dev) {
- emitSpecWarnings(collectContrastWarnings(scales, chartSpec.markType, theme), options.onWarn);
- }
-
- // Axisless marks (arc, waffle, calendar, parliament) don't use axes or
- // gridlines: arcs and parliament hemicycles are radial, waffles and calendars
- // compute their own grid geometry.
- const isRadial = isAxislessMark(chartSpec.markType);
-
- // Compute axes (skip for radial charts).
- // Sparkline mode skips axes by default unless the user explicitly opted into
- // an axis on a specific channel.
- const axisless = chartSpec.display === 'sparkline' || !economy.axes;
- const skipX = axisless && !chartSpec.userExplicit.xAxis;
- const skipY = axisless && !chartSpec.userExplicit.yAxis;
- const axes = isRadial
- ? { x: undefined, y: undefined }
- : computeAxes(scales, chartArea, strategy, theme, options.measureText, {
- data: renderSpec.data,
- preserveEnd:
- typeof (spec as ChartSpec).responsive === 'object'
- ? (
- (spec as ChartSpec)
- .responsive as import('@opendata-ai/openchart-core').ResponsiveConfig
- ).preserveEnd
- : false,
- encoding: renderSpec.encoding as Encoding,
- skipX,
- skipY,
- suppressGrid: !economy.gridlines,
- maxXTicks: economy.maxXTicks,
- markType: chartSpec.markType,
- totalWidth: options.width,
- // If the guardrail stripped chrome, the plan's y-ticks were sized
- // against the pre-strip (shorter) plot. Drop them so computeAxes
- // regenerates against the final (taller) chartArea.
- precomputedYTicks:
- !dims.chromeStripped && plan.yTickValues.length > 0 ? plan.yTickValues : undefined,
- });
-
- // Intentional post-hoc mutation: axes must resolve before we know the x-axis extent.
- const xAxisExtent = axes.x?.extent ?? 0;
- dims.chrome.bottomAnchorY = chartArea.y + chartArea.height + xAxisExtent;
-
- // INVARIANT 2 — computeGridlines mutates `axes` in place. Downstream consumers read
- // axes.y.gridlines off the same object. Do not introduce a copy-on-write.
- if (!isRadial) {
- computeGridlines(axes, chartArea);
- }
-
- // Get chart renderer and compute marks (using filtered data).
- const rendererKey = resolveRendererKey(
- renderSpec.markType,
- renderSpec.encoding,
- renderSpec.markDef,
- );
- const renderer = getChartRenderer(rendererKey);
- const marks: Mark[] = renderer
- ? renderer(renderSpec, scales, chartArea, strategy, theme, options.width)
- : [];
-
- if (marks.some((mark) => mark.type === 'line' && mark.label?.visible)) {
- emitSpecWarnings(
- ['[openchart] Legacy end-of-line labels are deprecated; use endpointLabels.'],
- options.onWarn,
- );
- }
-
- // Opt-in per-series fill patterns (mark.fillPattern: 'auto'). Mutates
- // filled marks in place; assignment is deterministic and theme-aware.
- applyFillPatterns(marks, chartSpec.markDef, theme);
-
- // Rendering backend for the point marks. Advisory metadata: only stamped on
- // the layout when it resolves to 'canvas', so an SVG-mode layout is byte
- // identical to what this function produced before the field existed.
- const markRenderModeWarnings: string[] = [];
- const markRenderMode = resolveMarkRenderMode(
- {
- requested: options.renderer,
- markType: chartSpec.markType,
- pointCount: marks.reduce((n, m) => (m.type === 'point' ? n + 1 : n), 0),
- display: chartSpec.display,
- faceted: false,
- layered: options.layered ?? false,
- },
- markRenderModeWarnings,
- );
- emitSpecWarnings(markRenderModeWarnings, options.onWarn);
-
- // Resolve "you draw it" geometry (pixel fromX, x-sample positions, target
- // line color/key). Reads marks so it must run after they're computed.
- const xFieldForYdi =
- chartSpec.encoding.x && 'field' in chartSpec.encoding.x
- ? (chartSpec.encoding.x.field as string | undefined)
- : undefined;
- const youDrawIt = resolveYouDrawIt(chartSpec.youDrawIt, marks, scales, chartArea, xFieldForYdi);
-
- // Compute the right-side endpoint labels column for multi-series line/area
- // charts. Reads `mark.dataPoints` so it must run AFTER marks are computed.
- // dimensions.ts already reserved the right margin via predictEndpointLabelsWidth.
- const endpointLabels = computeEndpointLabels(
- chartSpec,
- marks,
- theme,
- chartArea,
- strategy,
- plan.endpointLabelsDemoted,
- );
-
- // Place legend in its final position relative to the computed chart area.
- // effectiveAxisGap lifts a top legend above the inline-tick overhang zone
- // the margin stack reserved below it.
- const finalLegend = placeLegend(
- plan.legendContent,
- // Dimensions reserve both columns; place the legend after the resolved
- // endpoint column instead of drawing both at the plot's right edge.
- (plan.legendContent.position === 'right' || plan.legendContent.position === 'bottom-right') &&
- endpointLabels.entries.length > 0
- ? { ...chartArea, width: chartArea.width + ENDPOINT_COLUMN_GAP + endpointLabels.bounds.width }
- : chartArea,
- options.width,
- theme,
- plan.xAxisExtent,
- dims.effectiveAxisGap,
- );
-
- // Every legend on the chart. The color legend is primary (and stays in the
- // singular `legend` slot); a size legend joins it when a quantitative `size`
- // channel is encoded. Consumers that reserve space or hit-test must iterate
- // this rather than read `legend` -- see ChartLayout.legends.
- const sizeLegend = plan.sizeLegendContent
- ? placeSizeLegend(plan.sizeLegendContent, chartArea, theme, finalLegend)
- : null;
- const allLegends: LegendLayout[] = [finalLegend, ...(sizeLegend ? [sizeLegend] : [])];
-
- // Compute annotations from spec, passing legend + mark + brand bounds as obstacles
- const obstacles: PlacementObstacle[] = [];
- if (finalLegend.bounds.width > 0) {
- obstacles.push({ ...finalLegend.bounds, kind: 'legend' });
- }
- obstacles.push(...computeMarkObstacles(marks, scales));
-
- // Add visible data label bounds as obstacles so annotations avoid overlapping them
- for (const mark of marks) {
- if ('label' in mark && mark.label?.visible) {
- obstacles.push({ ...computeLabelBounds(mark.label), kind: 'data-label' });
- }
- }
-
- // Add brand watermark as an obstacle so annotations avoid overlapping it.
- const watermarkRect = computeWatermarkObstacle(dims, watermark, axes, theme);
- if (watermarkRect) obstacles.push({ ...watermarkRect, kind: 'watermark' });
-
- // Add endpoint label bounds as obstacles
- if (endpointLabels.entries.length > 0 && endpointLabels.bounds.width > 0) {
- obstacles.push({ ...endpointLabels.bounds, kind: 'endpoint-label' });
- }
-
- // The top chrome block (eyebrow / title / subtitle) is painted text, not empty
- // space. Auto-placed annotations anchored near the plot's top edge would
- // otherwise happily land on top of the subtitle.
- const chromeBottomY = dims.topPad + dims.chrome.topHeight;
- if (dims.chrome.topHeight > 0) {
- obstacles.push({
- x: 0,
- y: 0,
- width: dims.total.width,
- height: chromeBottomY,
- kind: 'chrome',
- });
- }
-
- const annotationMeasure: AnnotationMeasureTextFn = options.measureText
- ? (text, font) => options.measureText!(text, font.fontSize, font.fontWeight).width
- : heuristicMeasure;
-
- let annotations: ResolvedAnnotation[] = computeAnnotations(chartSpec, {
- scales,
- chartArea,
- strategy,
- isDark: theme.isDark,
- obstacles,
- svg: { width: dims.total.width, height: dims.total.height },
- measure: annotationMeasure,
- fontFamily: theme.fonts.family,
- colors: theme.colors,
- onWarn: (message) => emitSpecWarnings([message], options.onWarn),
- autoThin: chartSpec.autoThin,
- // Hand-placed blocks skip every nudge pass, so the clamp is the only lever
- // that keeps an author's offset from parking a callout on the title.
- topBound: dims.chrome.topHeight > 0 ? chromeBottomY : undefined,
- });
-
- // Dev-mode diagnostic: hand-placed annotations that sit on the data. Advisory
- // only -- a pinned block is never moved by the engine, so the fix is authorial.
- if (options.dev) {
- emitSpecWarnings(
- collectPinnedOverlapWarnings(
- annotations,
- chartSpec.annotations,
- obstacles,
- annotationMeasure,
- ),
- options.onWarn,
- );
- }
-
- // Auto-thinning: demote overlapping text annotations to footnote markers.
- // Pass the full spec annotations (not filtered to text-only) so index
- // alignment with the resolved array is preserved.
- let footnotes: import('@opendata-ai/openchart-core').AnnotationFootnote[] | undefined;
- if (chartSpec.autoThin && (annotations.length > 1 || chartSpec.compactAnnotationLimit != null)) {
- const result = thinAnnotations(
- annotations,
- chartSpec.annotations,
- annotationMeasure,
- chartArea,
- { x: 0, y: 0, width: dims.total.width, height: dims.total.height },
- options.width < 400 && options.height >= 200 ? chartSpec.compactAnnotationLimit : undefined,
- );
- annotations = result.annotations;
- if (result.footnotes.length > 0) {
- footnotes = wrapAnnotationNotes(
- result.footnotes,
- dims.total.width,
- theme,
- options.measureText,
- );
-
- // Source capacity normally covers this list. Keep a bounded fallback
- // for custom measurement functions. Frozen layers share the primary
- // plot and reconcile any larger reserve in their assembly pass.
- const reserve = computeNoteBandHeight(annotationNoteLineCount(footnotes), theme);
- const currentReserve = options.footnoteReserve ?? 0;
- if (
- !options.frozenChartArea &&
- reserve > currentReserve &&
- (options.footnotePass ?? 0) < MAX_FOOTNOTE_PASSES
- ) {
- return compileChartInternal(spec, {
- ...optionsInput,
- footnoteReserve: reserve,
- footnotePass: (options.footnotePass ?? 0) + 1,
- });
- }
- }
- }
-
- const responsive = (spec as ChartSpec).responsive;
- const wantsSummary = typeof responsive === 'object' && responsive.labelSummary;
- const seriesSummary =
- wantsSummary && endpointLabels.entries.length === 0
- ? computeEndpointLabels(chartSpec, marks, theme, chartArea, undefined, false).entries.map(
- (entry) => ({
- key: entry.seriesKey,
- text: `${entry.seriesKey} · ${entry.value}`,
- lines: wrapText(
- `${entry.seriesKey} · ${entry.value}`,
- theme.fonts.sizes.small,
- 500,
- Math.max(1, options.width - 2 * theme.spacing.padding),
- options.measureText,
- ).flatMap((line) => {
- // Break an unspaced identifier too: editorial labels may be URLs or codes.
- const max = Math.max(1, options.width - 2 * theme.spacing.padding);
- const chunks: string[] = [];
- let chunk = '';
- for (const character of line) {
- const candidate = chunk + character;
- if (chunk && measure(candidate, theme.fonts.sizes.small, 500) > max) {
- chunks.push(chunk);
- chunk = character;
- } else chunk = candidate;
- }
- if (chunk) chunks.push(chunk);
- return chunks;
- }),
- color: entry.color,
- }),
- )
- : [];
- if (seriesSummary.length) {
- const reserve = computeNoteBandHeight(
- Math.max(noteCapacity, annotationNoteLineCount(footnotes ?? [])) +
- seriesSummary.reduce((count, entry) => count + entry.lines.length, 0),
- theme,
- );
- const previousReserve = options.footnoteReserve ?? 0;
- if (
- !options.frozenChartArea &&
- reserve > previousReserve &&
- (options.footnotePass ?? 0) < MAX_FOOTNOTE_PASSES
- ) {
- return compileChartInternal(spec, {
- ...optionsInput,
- footnoteReserve: reserve,
- footnotePass: (options.footnotePass ?? 0) + 1,
- });
- }
- }
-
- // Compute tooltip descriptors from marks and encoding
- const tooltipDescriptors = computeTooltipDescriptors(chartSpec, marks);
-
- // Compute accessibility. An author-provided description (a11y.description,
- // or top-level `description` folded in by the sugar expansion) replaces the
- // auto-generated alt text; auto-generation stays the fallback.
- const altText =
- chartSpec.a11y?.description ??
- generateAltText(
- {
- mark: chartSpec.markType,
- data: chartSpec.data,
- encoding: chartSpec.encoding,
- chrome: chartSpec.chrome,
- } as ChartSpec,
- chartSpec.data,
- );
- const dataTableFallback = generateDataTable(
- {
- mark: chartSpec.markType,
- data: chartSpec.data,
- encoding: chartSpec.encoding,
- } as ChartSpec,
- chartSpec.data,
- );
-
- // Assign animationIndex for stagger ordering when animation is enabled
- assignAnimationIndices(marks, resolvedAnimation);
-
- const noteBandHeight = Math.max(
- options.footnoteReserve ?? 0,
- computeNoteBandHeight(
- Math.max(noteCapacity, annotationNoteLineCount(footnotes ?? [])) +
- seriesSummary.reduce((count, entry) => count + entry.lines.length, 0),
- theme,
- ),
- );
- const chrome = {
- ...dims.chrome,
- ...(noteBandHeight > 0 ? { noteBandHeight } : {}),
- ...(footnotes ? { footnotes } : {}),
- ...(seriesSummary.length ? { seriesSummary } : {}),
- };
-
- // Scale inversion anchors for edit-mode anchor drag (pixel -> data-space)
- const xInvert = resolveScaleInvert(scales.x);
- const yInvert = resolveScaleInvert(scales.y);
-
- const layout: ChartLayout = {
- area: chartArea,
- chrome,
- metrics: dims.metrics,
- ...(dims.seriesSearch ? { seriesSearch: dims.seriesSearch } : {}),
- ...(youDrawIt ? { youDrawIt } : {}),
- axes: {
- x: axes.x,
- y: axes.y,
- },
- marks,
- annotations,
- legend: finalLegend,
- legends: allLegends,
- ...(endpointLabels.entries.length > 0 ? { endpointLabels } : {}),
- tooltipDescriptors,
- a11y: {
- altText,
- dataTableFallback,
- ...(chartSpec.data.length > MAX_SR_TABLE_ROWS ? { totalRows: chartSpec.data.length } : {}),
- role: 'img',
- keyboardNavigable: marks.length > 0,
- ...(chartSpec.a11y?.hidden ? { hidden: true } : {}),
- },
- theme,
- dimensions: {
- width: options.width,
- // In chromeLayout 'grow' mode, computeDimensions grows total.height by the
- // chrome height so the plot keeps its full budget. In the default
- // 'subtract' mode total.height === options.height, so this is a no-op.
- height: dims.total.height,
- },
- animation: resolvedAnimation,
- watermark,
- display: chartSpec.display,
- crosshair,
- measureText: options.measureText,
- ...(xInvert ? { xInvert } : {}),
- ...(yInvert ? { yInvert } : {}),
- ...(markRenderMode === 'canvas' ? { markRenderMode } : {}),
- };
- if (options.onDiagnostic) {
- // Diagnostics inspect the same rows/fields as the compiled scene, including
- // data.values and derived channels. Fixes still target the authored document.
- const diagnosticSpec = {
- ...(expandedSpec as ChartSpec),
- data: chartSpec.data,
- encoding: chartSpec.encoding,
- } as ChartSpec;
- for (const diagnostic of diagnoseChart(diagnosticSpec, layout)) {
- if (diagnostic.fix) diagnostic.fix.precondition = JSON.stringify(spec);
- options.onDiagnostic(diagnostic);
- }
- }
- return layout;
-}
-
-// ---------------------------------------------------------------------------
-// Faceted (small-multiples) compilation
-// ---------------------------------------------------------------------------
-
-function compileFaceted(
- chartSpec: NormalizedChartSpec,
- renderSpec: NormalizedChartSpec,
- facetChannel: FacetChannel,
- facetDirection: 'row' | 'column' | 'wrap',
- options: CompileOptions,
- theme: ResolvedTheme,
- strategy: LayoutStrategy,
- watermark: boolean,
- resolvedAnimation: ResolvedAnimation | undefined,
- crosshair: boolean,
- rawSpec: unknown,
- optionsInput: CompileOptions,
-): ChartLayout {
- const measure = createMeasureFn(options.measureText);
-
- // Strip facet from encoding before passing to per-panel pipeline (panels
- // don't know they're faceted -- they compile as regular single charts).
- const panelEncoding = { ...renderSpec.encoding };
- delete (panelEncoding as Record).facet;
- const panelSpec = { ...renderSpec, encoding: panelEncoding };
- const panelChartSpec = { ...chartSpec, encoding: panelEncoding };
-
- // Partition data by facet field
- const groups = groupByField(renderSpec.data, facetChannel.field);
- const facetValues = Array.from(groups.keys());
- if (facetChannel.sort !== null) {
- const dir = facetChannel.sort === 'descending' ? -1 : 1;
- facetValues.sort((a, b) => dir * a.localeCompare(b));
- }
-
- // Compute figure-level chrome and dimensions using the full dataset
- // (no facet channel so it compiles normally). We use a representative
- // single-panel spec to get chrome + legend measurements.
- const plan = resolveLayoutPlan(
- panelChartSpec,
- panelSpec,
- options,
- theme,
- strategy,
- watermark,
- measure,
- );
- const legendStub = placeLegend(
- plan.legendContent,
- { x: 0, y: 0, width: options.width, height: options.height },
- options.width,
- theme,
- plan.xAxisExtent,
- );
- const dims = computeDimensions(
- panelChartSpec,
- options,
- legendStub,
- theme,
- strategy,
- watermark,
- plan,
- false,
- );
- const chartArea = dims.chartArea;
-
- // Same effectiveAxisGap threading as the regular (non-faceted) path.
- const finalLegend = placeLegend(
- plan.legendContent,
- chartArea,
- options.width,
- theme,
- plan.xAxisExtent,
- dims.effectiveAxisGap,
- );
-
- // Estimate x-axis extent (used as bottom reservation for outer axes)
- const bottomReservation = plan.xAxisExtent;
-
- // Compute facet grid geometry. The y-axis gutter needs no left reservation
- // here: chartArea's left margin already includes plan.leftGutter (applied by
- // computeDimensions), so leftmost-panel tick labels render into that margin
- // just like the single-chart path.
- const effectiveColumns = facetDirection === 'column' ? facetValues.length : facetChannel.columns;
- const facetAnnotationMeasure: AnnotationMeasureTextFn = options.measureText
- ? (text, font) => options.measureText!(text, font.fontSize, font.fontWeight).width
- : heuristicMeasure;
- const hasOutsideReferences = chartSpec.annotations.some(
- (a) => a.type === 'refline' && a.labelPlacement === 'outside' && !!a.label,
- );
- const referenceInsets = hasOutsideReferences
- ? {
- measure: (width: number) =>
- referenceLabelReserve(chartSpec.annotations, width, facetAnnotationMeasure),
- innerAxisWidth:
- (chartSpec.resolve?.scale?.y ?? (facetDirection === 'row' ? 'independent' : 'shared')) ===
- 'independent'
- ? plan.leftGutter
- : 0,
- }
- : undefined;
- const grid = computeFacetGrid(
- facetValues,
- effectiveColumns,
- chartArea,
- {
- left: 0,
- bottom: bottomReservation,
- },
- referenceInsets,
- );
-
- // If panels are too short, grow the compile height and restart (once).
- if (
- !optionsInput.facetHeightGrown &&
- grid.panels.length > 0 &&
- grid.panels[0].area.height < MIN_PANEL_HEIGHT
- ) {
- const neededChartHeight =
- grid.minimumHeight ?? facetMinHeight(facetValues.length, grid.columns);
- const heightGrowth = neededChartHeight - chartArea.height;
- if (heightGrowth > 0) {
- const grown = compileChartInternal(rawSpec, {
- ...optionsInput,
- height: options.height + heightGrowth,
- facetHeightGrown: true,
- });
- if (grown.chrome.grownHeight !== undefined) grown.chrome.grownHeight += heightGrowth;
- return grown;
- }
- }
-
- // Resolve scale: defaults depend on facet direction.
- // row: x shared (comparable values), y independent (different categories)
- // column: y shared (comparable values), x independent (different categories)
- // wrap: both shared (original facet behavior)
- const resolveConfig = chartSpec.resolve;
- const xDefault: ResolveMode = facetDirection === 'column' ? 'independent' : 'shared';
- const yDefault: ResolveMode = facetDirection === 'row' ? 'independent' : 'shared';
- const yResolve = resolveConfig?.scale?.y ?? yDefault;
- const xResolve = resolveConfig?.scale?.x ?? xDefault;
-
- // Compute shared scale domains from the full dataset (when shared)
- const fullScales = computeScales(panelSpec, chartArea, renderSpec.data);
- const sharedXDomain =
- xResolve === 'shared' && fullScales.x ? fullScales.x.scale.domain() : undefined;
- const sharedYDomain =
- yResolve === 'shared' && fullScales.y ? fullScales.y.scale.domain() : undefined;
-
- const isRadial = isAxislessMark(chartSpec.markType);
- const rendererKey = resolveRendererKey(
- renderSpec.markType,
- panelEncoding as Encoding,
- renderSpec.markDef,
- );
- const renderer = getChartRenderer(rendererKey);
-
- const allMarks: Mark[] = [];
- const panelLayouts: FacetPanelLayout[] = [];
- // Footnotes pool across panels into one figure-level list (the renderer draws
- // a single list below the grid from `chrome.footnotes`), so the numbering is
- // continuous and each panel's markers index into it.
- const allFootnotes: import('@opendata-ai/openchart-core').AnnotationFootnote[] = [];
- const footnoteNumberBySpecIndex = new Map();
-
- for (const gridPanel of grid.panels) {
- const panelData = groups.get(gridPanel.key) ?? [];
- const panelSpecLocal = { ...panelSpec, data: panelData };
-
- // Inject shared domains via scale.domain on the encoding channels
- let panelEnc = { ...panelEncoding } as Record;
- if (sharedXDomain && panelEnc.x) {
- const xCh = panelEnc.x as Record;
- panelEnc = {
- ...panelEnc,
- x: { ...xCh, scale: { ...((xCh.scale as object) ?? {}), domain: sharedXDomain } },
- };
- }
- if (sharedYDomain && panelEnc.y) {
- const yCh = panelEnc.y as Record;
- panelEnc = {
- ...panelEnc,
- y: { ...yCh, scale: { ...((yCh.scale as object) ?? {}), domain: sharedYDomain } },
- };
- }
- const panelSpecWithDomains = {
- ...panelSpecLocal,
- encoding: panelEnc as unknown as typeof panelSpec.encoding,
- };
-
- // Compute scales for this panel's area
- const panelScales = computeScales(panelSpecWithDomains, gridPanel.area, panelData);
- applyColorScaleRange(
- panelScales,
- panelSpecWithDomains.encoding,
- theme,
- renderSpec.highlight,
- chartSpec.markType,
- );
- panelScales.defaultColor =
- chartSpec.markDef.fill ??
- chartSpec.markDef.stroke ??
- defaultSeriesColor(theme, chartSpec.markType);
-
- // Dev-mode contrast diagnostics: panels share one palette resolution, so
- // checking the first panel covers the figure without repeating warnings.
- if (options.dev && panelLayouts.length === 0) {
- emitSpecWarnings(
- collectContrastWarnings(panelScales, chartSpec.markType, theme),
- options.onWarn,
- );
- }
-
- const isLeftCol = gridPanel.col === 0;
-
- const panelAxes = isRadial
- ? { x: undefined, y: undefined }
- : computeAxes(panelScales, gridPanel.area, strategy, theme, options.measureText, {
- data: panelData,
- encoding: panelSpecWithDomains.encoding as Encoding,
- markType: chartSpec.markType,
- // Outside-label cells use figure coordinates throughout, including
- // the tick-label clamp. Keep the legacy path unchanged in 8.x.
- totalWidth: gridPanel.referenceContainer
- ? gridPanel.area.x + gridPanel.area.width
- : gridPanel.area.width,
- });
-
- if (!isRadial) {
- computeGridlines(panelAxes, gridPanel.area);
- }
-
- // Strip y-axis tick labels from non-leftmost panels (gridlines stay).
- if (yResolve === 'shared' && !isLeftCol && panelAxes.y) {
- panelAxes.y = {
- ...panelAxes.y,
- ticks: [],
- label: undefined,
- domainLine: false,
- tickMarks: false,
- };
- }
-
- // Strip x-axis tick labels from non-bottom panels for row faceting.
- const isBottomRow = gridPanel.row === grid.rows - 1;
- if (xResolve === 'shared' && facetDirection === 'row' && !isBottomRow && panelAxes.x) {
- panelAxes.x = {
- ...panelAxes.x,
- ticks: [],
- label: undefined,
- domainLine: false,
- tickMarks: false,
- };
- }
-
- // Compute marks for this panel
- const panelMarks: Mark[] = renderer
- ? renderer(
- panelSpecWithDomains,
- panelScales,
- gridPanel.area,
- strategy,
- theme,
- gridPanel.area.width,
- )
- : [];
-
- // Compute annotations for this panel
- const annotationMeasure: AnnotationMeasureTextFn = options.measureText
- ? (text, font) => options.measureText!(text, font.fontSize, font.fontWeight).width
- : heuristicMeasure;
- let panelAnnotations = computeAnnotations(panelSpecWithDomains, {
- scales: panelScales,
- chartArea: gridPanel.area,
- referenceContainer: gridPanel.referenceContainer,
- referenceReserve: gridPanel.referenceContainer
- ? referenceLabelReserve(
- chartSpec.annotations,
- gridPanel.referenceContainer.width,
- facetAnnotationMeasure,
- )
- : undefined,
- strategy,
- isDark: theme.isDark,
- obstacles: [],
- svg: { width: dims.total.width, height: dims.total.height },
- measure: annotationMeasure,
- fontFamily: theme.fonts.family,
- colors: theme.colors,
- onWarn: (message) => emitSpecWarnings([message], options.onWarn),
- autoThin: panelSpecWithDomains.autoThin,
- topBound: dims.chrome.topHeight > 0 ? dims.topPad + dims.chrome.topHeight : undefined,
- });
-
- // Auto-thinning, per panel. A panel is its own little chart: the box a label
- // must stay inside is the panel, not the figure. Thinning against the whole
- // SVG (what the single-chart path passes) would let a label from the left
- // column sprawl across its neighbour and still count as "fits".
- //
- // The spec's annotations resolve into *every* panel, so the same annotation
- // can demote in more than one. The footnote list is figure-level and its text
- // is per-spec-annotation, so number by spec index and reuse that number in
- // every panel that demoted it -- otherwise the list repeats itself once per
- // panel.
- if (
- chartSpec.autoThin &&
- (panelAnnotations.length > 1 || chartSpec.compactAnnotationLimit != null)
- ) {
- const thinned = thinAnnotations(
- panelAnnotations,
- chartSpec.annotations,
- annotationMeasure,
- gridPanel.area,
- gridPanel.area,
- options.width < 400 && options.height >= 200 ? chartSpec.compactAnnotationLimit : undefined,
- );
- panelAnnotations = thinned.annotations;
- // `thinAnnotations` numbers footnotes 1..n *within the panel it was given*,
- // so restamp each demoted annotation with its figure-level number. Take the
- // text from `thinned.footnotes` (the 1-based panel-local index it just
- // assigned) rather than re-reading `label.text`: thinning owns that string,
- // and re-deriving it here would be a second producer to drift out of sync.
- //
- // Key on `specIndex`, NOT on position: with `resolve.scale.x: 'independent'`
- // each panel derives its own domain, so an annotation can resolve in one
- // panel and be dropped in another. Position i then means a different spec
- // annotation in each panel, and the markers point at other panels' text.
- for (const a of panelAnnotations) {
- const panelIndex = a.footnoteIndex;
- if (panelIndex == null || a.specIndex === undefined) continue;
- let num = footnoteNumberBySpecIndex.get(a.specIndex);
- if (num === undefined) {
- num = allFootnotes.length + 1;
- footnoteNumberBySpecIndex.set(a.specIndex, num);
- allFootnotes.push({
- specIndex: a.specIndex,
- ...(a.id !== undefined ? { id: a.id } : {}),
- index: num,
- text: thinned.footnotes[panelIndex - 1]?.text ?? '',
- });
- }
- a.footnoteIndex = num;
- }
- }
-
- allMarks.push(...panelMarks);
-
- panelLayouts.push({
- key: gridPanel.key,
- area: gridPanel.area,
- marks: panelMarks,
- axes: { x: panelAxes.x, y: panelAxes.y },
- annotations: panelAnnotations,
- header: {
- text: gridPanel.key,
- x: facetDirection === 'row' ? gridPanel.area.x : gridPanel.headerPos.x,
- y: gridPanel.headerPos.y,
- fontSize: theme.fonts.sizes.body,
- fontWeight: theme.fonts.weights.medium,
- ...(facetDirection === 'row' ? { textAnchor: 'start' as const } : {}),
- },
- });
- }
-
- // Facets share the source-capacity preflight. Reconcile an unexpected
- // measurement overflow once at figure level, never once per panel.
- const wrappedFootnotes = wrapAnnotationNotes(
- allFootnotes,
- dims.total.width,
- theme,
- options.measureText,
- );
- if (allFootnotes.length > 0 && !options.frozenChartArea) {
- const reserve = computeNoteBandHeight(annotationNoteLineCount(wrappedFootnotes), theme);
- const currentReserve = options.footnoteReserve ?? 0;
- if (reserve > currentReserve && (options.footnotePass ?? 0) < MAX_FOOTNOTE_PASSES) {
- return compileChartInternal(rawSpec, {
- ...optionsInput,
- footnoteReserve: reserve,
- footnotePass: (options.footnotePass ?? 0) + 1,
- });
- }
- }
-
- // Compute tooltip descriptors from all marks across panels
- const tooltipDescriptors = computeTooltipDescriptors(panelChartSpec, allMarks);
-
- if (allMarks.some((mark) => mark.type === 'line' && mark.label?.visible)) {
- emitSpecWarnings(
- ['[openchart] Legacy end-of-line labels are deprecated; use endpointLabels.'],
- options.onWarn,
- );
- }
-
- // Opt-in per-series fill patterns across all panels at once so a series
- // gets the same pattern in every panel.
- applyFillPatterns(allMarks, chartSpec.markDef, theme);
-
- // A11y: mention faceting in alt text. An author description is used
- // verbatim (the author already describes the whole figure).
- const panelCount = facetValues.length;
- const baseAltText = generateAltText(
- {
- mark: chartSpec.markType,
- data: chartSpec.data,
- encoding: chartSpec.encoding,
- chrome: chartSpec.chrome,
- } as ChartSpec,
- chartSpec.data,
- );
- const altText =
- chartSpec.a11y?.description ??
- `${baseAltText} Faceted into ${panelCount} panels by ${facetChannel.field}.`;
-
- const dataTableFallback = generateDataTable(
- {
- mark: chartSpec.markType,
- data: chartSpec.data,
- encoding: chartSpec.encoding,
- } as ChartSpec,
- chartSpec.data,
- );
-
- // Assign animation indices across all panels
- assignAnimationIndices(allMarks, resolvedAnimation);
-
- // Rendering backend, resolved against the pooled panel marks. The canvas
- // layer cannot express a facet grid, so this always answers 'svg' and never
- // stamps the layout; it runs purely so the refusal is reported -- an explicit
- // renderer: 'canvas' gets told it was ignored, and a dense facet grid gets
- // told why it is painting every point as a DOM node.
- const facetRenderModeWarnings: string[] = [];
- resolveMarkRenderMode(
- {
- requested: options.renderer,
- markType: chartSpec.markType,
- pointCount: allMarks.reduce((n, m) => (m.type === 'point' ? n + 1 : n), 0),
- display: chartSpec.display,
- faceted: true,
- layered: options.layered ?? false,
- },
- facetRenderModeWarnings,
- );
- emitSpecWarnings(facetRenderModeWarnings, options.onWarn);
-
- // Figure-level axes are null (axes live in panels)
- // Figure-level marks are the union of all panel marks (for tooltip/keyboard nav)
- return {
- area: chartArea,
- chrome: {
- ...dims.chrome,
- noteBandHeight: Math.max(
- options.footnoteReserve ?? 0,
- computeNoteBandHeight(annotationNoteLineCount(wrappedFootnotes), theme),
- ),
- ...(allFootnotes.length > 0 ? { footnotes: wrappedFootnotes } : {}),
- },
- metrics: dims.metrics,
- ...(dims.seriesSearch ? { seriesSearch: dims.seriesSearch } : {}),
- axes: { x: undefined, y: undefined },
- marks: allMarks,
- annotations: [],
- legend: finalLegend,
- // Facets share one color legend across the grid; the size channel isn't
- // supported in facet specs, so there is no second legend to carry.
- legends: [finalLegend],
- tooltipDescriptors,
- a11y: {
- altText,
- dataTableFallback,
- ...(chartSpec.data.length > MAX_SR_TABLE_ROWS ? { totalRows: chartSpec.data.length } : {}),
- role: 'img',
- keyboardNavigable: allMarks.length > 0,
- ...(chartSpec.a11y?.hidden ? { hidden: true } : {}),
- },
- theme,
- dimensions: { width: dims.total.width, height: dims.total.height },
- animation: resolvedAnimation,
- watermark,
- display: chartSpec.display,
- crosshair,
- measureText: options.measureText,
- facet: {
- panels: panelLayouts,
- facetField: facetChannel.field,
- columns: grid.columns,
- sharedScales: yResolve === 'shared' && xResolve === 'shared',
- direction: facetDirection,
- },
- };
-}
-
// ---------------------------------------------------------------------------
// Layer compilation
// ---------------------------------------------------------------------------
@@ -1726,6 +86,8 @@ export function compileLayer(spec: LayerSpec, options: CompileOptions): ChartLay
) as unknown as LayerSpec;
emitSpecWarnings(sugarWarnings, options.onWarn);
const resolvedOptions = applySpecSize(expanded, options);
+ // Leaves can each report the overflow of the shared note band; keep one.
+ let noteOverflow: DesignDiagnostic | undefined;
const layout = compileLayerImpl(
expanded,
{
@@ -1734,12 +96,13 @@ export function compileLayer(spec: LayerSpec, options: CompileOptions): ChartLay
? (diagnostic) => {
// Report note exhaustion against the assembled figure, not intermediate leaves.
if (diagnostic.code !== 'layout.note-capacity') options.onDiagnostic!(diagnostic);
+ else if (isNoteOverflowDiagnostic(diagnostic)) noteOverflow ??= diagnostic;
}
: undefined,
},
compileChartInternal,
);
- const diagnostic = noteCapacityDiagnostic(layout);
+ const diagnostic = noteCapacityDiagnostic(layout) ?? noteOverflow;
if (diagnostic) options.onDiagnostic?.(diagnostic);
return layout;
}
@@ -1763,8 +126,7 @@ export function compileTable(spec: unknown, options: CompileTableOptions): Table
const { spec: normalized, warnings: normalizeWarnings } = compileSpec(spec);
emitSpecWarnings(normalizeWarnings, options.onWarn);
- const normType =
- 'type' in normalized ? (normalized as unknown as Record).type : undefined;
+ const normType = 'type' in normalized ? normalized.type : undefined;
if (normType !== 'table') {
throw new Error(`compileTable received a non-table spec. Use compileChart instead.`);
}
@@ -1772,12 +134,7 @@ export function compileTable(spec: unknown, options: CompileTableOptions): Table
const tableSpec = normalized as NormalizedTableSpec;
// Resolve theme: merge spec-level theme with options-level overrides
- emitThemeWarnings(options.theme, options.onWarn);
- const mergedThemeConfig = mergeThemeConfig(tableSpec.theme, options.theme);
- let theme: ResolvedTheme = resolveTheme(mergedThemeConfig);
- if (options.darkMode) {
- theme = adaptTheme(theme);
- }
+ const theme = resolveCompileTheme(tableSpec.theme, options);
// Resolve watermark: spec-level wins, then options, then default true
const rawWatermark = (spec as Record).watermark;
@@ -1823,10 +180,7 @@ export function compileGraph(spec: unknown, options: CompileOptions): GraphCompi
* @returns SankeyLayout with computed positions and visual properties.
* @throws Error if spec is invalid or not a sankey type.
*/
-export function compileSankey(
- spec: unknown,
- options: CompileOptions,
-): import('@opendata-ai/openchart-core').SankeyLayout {
+export function compileSankey(spec: unknown, options: CompileOptions): SankeyLayout {
return compileSankeyImpl(spec, options);
}
@@ -1846,10 +200,7 @@ export function compileSankey(
* @returns TileMapLayout with computed positions and visual properties.
* @throws Error if spec is invalid or not a tilemap type.
*/
-export function compileTileMap(
- spec: unknown,
- options: CompileOptions,
-): import('@opendata-ai/openchart-core').TileMapLayout {
+export function compileTileMap(spec: unknown, options: CompileOptions): TileMapLayout {
return compileTileMapImpl(spec, options);
}
@@ -1869,10 +220,7 @@ export function compileTileMap(
* @returns GeoMapLayout with computed positions and visual properties.
* @throws Error if spec is invalid or not a map type.
*/
-export function compileGeoMap(
- spec: unknown,
- options: CompileOptions,
-): import('@opendata-ai/openchart-core').GeoMapLayout {
+export function compileGeoMap(spec: unknown, options: CompileOptions): GeoMapLayout {
return compileGeoMapImpl(spec, options);
}
@@ -1892,9 +240,6 @@ export function compileGeoMap(
* @returns BarListLayout with computed positions and visual properties.
* @throws Error if spec is invalid or not a barlist type.
*/
-export function compileBarList(
- spec: unknown,
- options: CompileOptions,
-): import('@opendata-ai/openchart-core').BarListLayout {
+export function compileBarList(spec: unknown, options: CompileOptions): BarListLayout {
return compileBarListImpl(spec, options);
}
diff --git a/packages/engine/src/compile/__tests__/fill-patterns.test.ts b/packages/engine/src/compile/__tests__/fill-patterns.test.ts
index 85273cc1..20a87fba 100644
--- a/packages/engine/src/compile/__tests__/fill-patterns.test.ts
+++ b/packages/engine/src/compile/__tests__/fill-patterns.test.ts
@@ -99,7 +99,8 @@ describe('applyFillPatterns via compileChart', () => {
expect(thin.length).toBeGreaterThan(0);
expect(thick.length).toBeGreaterThan(0);
for (const rect of thin) expect(rect.pattern).toBeUndefined();
- for (const rect of thick) expect(rect.pattern).toBeDefined();
+ // The pattern tile is laid over the series' own solid color.
+ for (const rect of thick) expect(rect.pattern!.base).toBe(rect.fill);
});
it('assignment does not shift when individual marks are too small', () => {
@@ -138,7 +139,7 @@ describe('fill patterns on arcs', () => {
],
encoding: {
color: { field: 'browser', type: 'nominal' },
- y: { field: 'share', type: 'quantitative' },
+ theta: { field: 'share', type: 'quantitative' },
},
};
const layout = compileChart(spec, OPTS);
diff --git a/packages/engine/src/compile/__tests__/watermark-obstacle.test.ts b/packages/engine/src/compile/__tests__/watermark-obstacle.test.ts
index 05fdb496..d26e501e 100644
--- a/packages/engine/src/compile/__tests__/watermark-obstacle.test.ts
+++ b/packages/engine/src/compile/__tests__/watermark-obstacle.test.ts
@@ -1,93 +1,63 @@
-import type { AxisLayout, Rect, ResolvedChrome, ResolvedTheme } from '@opendata-ai/openchart-core';
-import { BRAND_RESERVE_WIDTH, resolveTheme } from '@opendata-ai/openchart-core';
+import type { ResolvedChrome } from '@opendata-ai/openchart-core';
+import { computeChrome, resolveTheme } from '@opendata-ai/openchart-core';
import { describe, expect, it } from 'vitest';
-import type { AxesResult } from '../../layout/axes';
import type { LayoutDimensions } from '../../layout/dimensions';
import { computeWatermarkObstacle } from '../watermark-obstacle';
-const theme: ResolvedTheme = resolveTheme();
-
-function makeDims(overrides: Partial = {}): LayoutDimensions {
- const total: Rect = { x: 0, y: 0, width: 800, height: 500 };
- const chartArea: Rect = { x: 60, y: 80, width: 700, height: 360 };
- const chrome: ResolvedChrome = {
- topHeight: 80,
- bottomHeight: 40,
- } as ResolvedChrome;
- return {
- total,
- chartArea,
+const theme = resolveTheme();
+const measure = (text: string, fontSize: number) => ({
+ width: (text.length * fontSize) / 2,
+ height: fontSize,
+});
+const dims = (chrome: ResolvedChrome, noteBand = 0) =>
+ ({
+ total: { x: 0, y: 0, width: 800, height: 500 },
+ chartArea: { x: 60, y: 80, width: 700, height: 360 },
chrome,
- margins: { top: 80, right: 40, bottom: 40, left: 60 },
+ noteBand,
theme,
- ...overrides,
- };
-}
-
-function makeAxis(label?: string): AxisLayout {
- return {
- ticks: [],
- gridlines: [],
- label,
- tickLabelStyle: {
- fontFamily: theme.fonts.family,
- fontSize: 12,
- fontWeight: 400,
- fill: '#000',
- lineHeight: 1.2,
- },
- start: { x: 0, y: 0 },
- end: { x: 100, y: 0 },
- };
-}
-
-describe('computeWatermarkObstacle', () => {
- it('returns null when watermark is disabled', () => {
- const dims = makeDims();
- const axes: AxesResult = { x: makeAxis(), y: makeAxis() };
- expect(computeWatermarkObstacle(dims, false, axes, theme)).toBeNull();
- });
-
- it('sits below the x-axis when no bottom chrome is present', () => {
- const dims = makeDims();
- const axes: AxesResult = { x: makeAxis('value'), y: makeAxis() };
- const rect = computeWatermarkObstacle(dims, true, axes, theme);
- expect(rect).not.toBeNull();
- // Right-aligned: total.width - padding - BRAND_RESERVE_WIDTH
- expect(rect!.x).toBe(dims.total.width - theme.spacing.padding - BRAND_RESERVE_WIDTH);
- expect(rect!.width).toBe(BRAND_RESERVE_WIDTH);
- expect(rect!.height).toBe(30);
- // Watermark sits below chart area, offset by x-axis extent + chartToFooter
- const expectedY = dims.chartArea.y + dims.chartArea.height + 48 + theme.spacing.chartToFooter;
- expect(rect!.y).toBe(expectedY);
+ }) as LayoutDimensions;
+describe('measured attribution obstacle', () => {
+ it('uses the resolved default mark bounds and the actual axis/footer anchor', () => {
+ const chrome = computeChrome({ source: 'Source' }, theme, 800, measure);
+ chrome.bottomAnchorY = 457;
+ const mark = chrome.watermark!;
+ expect(computeWatermarkObstacle(dims(chrome, 34), true, {}, theme, measure)).toEqual({
+ x: mark.x - mark.maxWidth,
+ y: 457 + 34 + mark.y,
+ width: mark.maxWidth,
+ height: mark.style.fontSize * mark.style.lineHeight!,
+ });
});
-
- it('aligns with bottom chrome when a source element is present', () => {
- const sourceY = 20;
- const dims = makeDims({
- chrome: {
- topHeight: 80,
- bottomHeight: 40,
- source: {
- text: 'Source: Test',
- x: 10,
- y: sourceY,
- maxWidth: 500,
- style: {
- fontFamily: theme.fonts.family,
- fontSize: 10,
- fontWeight: 400,
- fill: '#666',
- lineHeight: 1.2,
- },
+ it('reserves custom brand text even with default watermark disabled, including wrapped lines and explicit note height', () => {
+ const chrome = computeChrome(
+ {
+ brand: {
+ text: 'Custom brand\nSecond line',
+ offset: { dx: -10, dy: 6 },
+ style: { fontSize: 20 },
},
- } as ResolvedChrome,
+ },
+ theme,
+ 800,
+ measure,
+ 'full',
+ undefined,
+ false,
+ );
+ chrome.bottomAnchorY = 450;
+ chrome.noteBandHeight = 48;
+ const mark = chrome.brand!;
+ expect(computeWatermarkObstacle(dims(chrome, 20), false, {}, theme, measure)).toEqual({
+ x: mark.x - mark.maxWidth,
+ y: 450 + 48 + mark.y,
+ width: mark.maxWidth,
+ height: 2 * mark.style.fontSize * mark.style.lineHeight!,
});
- const axes: AxesResult = { x: makeAxis(), y: makeAxis() };
- const rect = computeWatermarkObstacle(dims, true, axes, theme);
- expect(rect).not.toBeNull();
- // With axis present but no label, extent is 26.
- // Y is chartArea.y + chartArea.height + 26 + sourceY
- expect(rect!.y).toBe(dims.chartArea.y + dims.chartArea.height + 26 + sourceY);
+ });
+ it('returns no obstacle without a resolved mark, and ignores guessed axis extents', () => {
+ expect(
+ computeWatermarkObstacle(dims({ topHeight: 0, bottomHeight: 0 }), true, {}, theme, measure),
+ ).toBeNull();
});
});
diff --git a/packages/engine/src/compile/accessibility.ts b/packages/engine/src/compile/accessibility.ts
new file mode 100644
index 00000000..b28db6a8
--- /dev/null
+++ b/packages/engine/src/compile/accessibility.ts
@@ -0,0 +1,45 @@
+import type { ChartLayout, ChartSpec, Mark } from '@opendata-ai/openchart-core';
+import { generateAltText, generateDataTable, MAX_SR_TABLE_ROWS } from '@opendata-ai/openchart-core';
+import type { NormalizedChartSpec } from '../compiler/types';
+
+/**
+ * Accessibility for a chart. An author-provided description (a11y.description,
+ * or top-level `description` folded in by the sugar expansion) replaces the
+ * auto-generated alt text; auto-generation stays the fallback. A faceted
+ * figure mentions its panels in the generated text.
+ */
+export function buildChartA11y(
+ chartSpec: NormalizedChartSpec,
+ marks: Mark[],
+ facet?: { panelCount: number; field: string },
+): ChartLayout['a11y'] {
+ const generated = () => {
+ const base = generateAltText(
+ {
+ mark: chartSpec.markType,
+ data: chartSpec.data,
+ encoding: chartSpec.encoding,
+ chrome: chartSpec.chrome,
+ } as ChartSpec,
+ chartSpec.data,
+ );
+ return facet ? `${base} Faceted into ${facet.panelCount} panels by ${facet.field}.` : base;
+ };
+ const altText = chartSpec.a11y?.description ?? generated();
+ const dataTableFallback = generateDataTable(
+ {
+ mark: chartSpec.markType,
+ data: chartSpec.data,
+ encoding: chartSpec.encoding,
+ } as ChartSpec,
+ chartSpec.data,
+ );
+ return {
+ altText,
+ dataTableFallback,
+ ...(chartSpec.data.length > MAX_SR_TABLE_ROWS ? { totalRows: chartSpec.data.length } : {}),
+ role: 'img',
+ keyboardNavigable: marks.length > 0,
+ ...(chartSpec.a11y?.hidden ? { hidden: true } : {}),
+ };
+}
diff --git a/packages/engine/src/compile/annotation-obstacles.ts b/packages/engine/src/compile/annotation-obstacles.ts
new file mode 100644
index 00000000..b0fa5f62
--- /dev/null
+++ b/packages/engine/src/compile/annotation-obstacles.ts
@@ -0,0 +1,74 @@
+/**
+ * Everything auto-placed annotations must avoid on a single chart: the legend,
+ * the marks, visible data labels, the brand watermark, the endpoint column,
+ * and the top chrome block.
+ */
+
+import type {
+ EndpointLabelsLayout,
+ LegendLayout,
+ Mark,
+ MeasureTextFn,
+ ResolvedTheme,
+} from '@opendata-ai/openchart-core';
+import { computeLabelBounds } from '@opendata-ai/openchart-core';
+import type { PlacementObstacle } from '../annotations/placement';
+import { computeMarkObstacles } from '../charts/post-process';
+import type { AxesResult } from '../layout/axes';
+import type { LayoutDimensions } from '../layout/dimensions';
+import type { ResolvedScales } from '../layout/scales';
+import { computeWatermarkObstacle } from './watermark-obstacle';
+
+export function collectAnnotationObstacles(params: {
+ legend: LegendLayout;
+ marks: Mark[];
+ scales: ResolvedScales;
+ dims: LayoutDimensions;
+ watermark: boolean;
+ axes: AxesResult;
+ theme: ResolvedTheme;
+ endpointLabels: EndpointLabelsLayout;
+ measureText?: MeasureTextFn;
+}): PlacementObstacle[] {
+ const { legend, marks, scales, dims, watermark, axes, theme, endpointLabels } = params;
+ const obstacles: PlacementObstacle[] = [];
+ if (legend.bounds.width > 0) {
+ obstacles.push({ ...legend.bounds, kind: 'legend' });
+ }
+ obstacles.push(...computeMarkObstacles(marks, scales));
+
+ // Add visible data label bounds as obstacles so annotations avoid overlapping them
+ for (const mark of marks) {
+ if ('label' in mark && mark.label?.visible) {
+ obstacles.push({ ...computeLabelBounds(mark.label), kind: 'data-label' });
+ }
+ }
+
+ // Add brand watermark as an obstacle so annotations avoid overlapping it.
+ const watermarkRect = computeWatermarkObstacle(dims, watermark, axes, theme, params.measureText);
+ if (watermarkRect) obstacles.push({ ...watermarkRect, kind: 'watermark' });
+
+ // Add endpoint label bounds as obstacles
+ if (endpointLabels.entries.length > 0 && endpointLabels.bounds.width > 0) {
+ obstacles.push({ ...endpointLabels.bounds, kind: 'endpoint-label' });
+ }
+
+ // The top chrome block (eyebrow / title / subtitle) is painted text, not empty
+ // space. Auto-placed annotations anchored near the plot's top edge would
+ // otherwise happily land on top of the subtitle.
+ if (dims.chrome.topHeight > 0) {
+ obstacles.push({
+ x: 0,
+ y: 0,
+ width: dims.total.width,
+ height: chromeBottomY(dims),
+ kind: 'chrome',
+ });
+ }
+ return obstacles;
+}
+
+/** Bottom edge of the top chrome block (eyebrow / title / subtitle). */
+export function chromeBottomY(dims: LayoutDimensions): number {
+ return dims.topPad + dims.chrome.topHeight;
+}
diff --git a/packages/engine/src/compile/breakpoint-overrides.ts b/packages/engine/src/compile/breakpoint-overrides.ts
new file mode 100644
index 00000000..fd414a98
--- /dev/null
+++ b/packages/engine/src/compile/breakpoint-overrides.ts
@@ -0,0 +1,198 @@
+/**
+ * Breakpoint-conditional spec overrides (`spec.overrides[breakpoint]`).
+ *
+ * The user-explicit descriptor is captured before any override applies, so it
+ * reflects the union of "user wrote this at top level" and "user wrote this in
+ * the active breakpoint override."
+ */
+
+import type { ChartSpecOverride } from '@opendata-ai/openchart-core';
+import { normalizeAnnotations } from '../compiler/normalize';
+import type { NormalizedChartSpec } from '../compiler/types';
+import type { InternalCompileOptions } from './internal-options';
+import { emitSpecWarnings } from './spec-sugar';
+
+/** One breakpoint's override block, with the encoding left open for merging. */
+export type BreakpointOverride = ChartSpecOverride & { encoding?: unknown };
+
+type AxisEncoding = { x?: { axis?: unknown }; y?: { axis?: unknown } } | undefined;
+
+// chrome: {} (empty object) is not "explicit" — it's an idiom users write to
+// silence defaults. Require at least one chrome key set to count as opt-in.
+const hasChromeKeys = (v: unknown): boolean =>
+ !!v && typeof v === 'object' && Object.keys(v as Record).length > 0;
+
+/**
+ * Build the userExplicit descriptor. Sparkline display mode reads this to
+ * decide whether to suppress chrome/axes/legend/etc. by default vs. respecting
+ * an explicit user opt-in. Precedence: explicit at any level wins.
+ */
+export function collectUserExplicit(
+ rawSpec: Record,
+ bp: BreakpointOverride | undefined,
+): NormalizedChartSpec['userExplicit'] {
+ const rawEncoding = rawSpec.encoding as AxisEncoding;
+ const bpEncoding = bp?.encoding as AxisEncoding;
+ return {
+ chrome: hasChromeKeys(rawSpec.chrome) || hasChromeKeys(bp?.chrome),
+ legend: rawSpec.legend !== undefined || bp?.legend !== undefined,
+ endpointLabels: rawSpec.endpointLabels !== undefined || bp?.endpointLabels !== undefined,
+ // Axis explicitness tracks "user opted IN to render the axis" — `axis: false`
+ // is an explicit opt-OUT and must not flip these flags on. Sparkline mode
+ // reads them to decide whether to reserve axis space; treating `false` as
+ // explicit would leave a phantom gutter for an axis that won't render.
+ xAxis:
+ (rawEncoding?.x?.axis !== undefined && rawEncoding.x.axis !== false) ||
+ (bpEncoding?.x?.axis !== undefined && bpEncoding.x.axis !== false),
+ yAxis:
+ (rawEncoding?.y?.axis !== undefined && rawEncoding.y.axis !== false) ||
+ (bpEncoding?.y?.axis !== undefined && bpEncoding.y.axis !== false),
+ labels: rawSpec.labels !== undefined || bp?.labels !== undefined,
+ animation: rawSpec.animation !== undefined || bp?.animation !== undefined,
+ watermark: rawSpec.watermark !== undefined || bp?.watermark !== undefined,
+ crosshair: rawSpec.crosshair !== undefined || bp?.crosshair !== undefined,
+ };
+}
+
+/** Result of applying one breakpoint override. */
+export interface AppliedOverride {
+ chartSpec: NormalizedChartSpec;
+ /** The override authored annotations, so they render inline at this breakpoint. */
+ annotationsInline: boolean;
+ /** Watermark the override sets, when the host left it to the spec. */
+ watermark?: boolean;
+}
+
+/**
+ * Merge a breakpoint override into a normalized chart spec. Channels merge
+ * per-key, and `axis` and `scale` deep-merge one level so a breakpoint can set
+ * `axis: { title: 'foo' }` without dropping the base spec's `axis.tickCount`.
+ */
+export function applyBreakpointOverride(
+ base: NormalizedChartSpec,
+ bp: BreakpointOverride,
+ rawSpec: Record,
+ options: InternalCompileOptions,
+): AppliedOverride {
+ let chartSpec = base;
+ let annotationsInline = false;
+ let watermark: boolean | undefined;
+
+ if (bp.endpointLabels !== undefined)
+ chartSpec = { ...chartSpec, endpointLabels: bp.endpointLabels };
+ if (bp.chrome) {
+ chartSpec = {
+ ...chartSpec,
+ chrome: {
+ ...chartSpec.chrome,
+ ...(bp.chrome as NormalizedChartSpec['chrome']),
+ },
+ };
+ }
+ if (bp.labels !== undefined) {
+ if (typeof bp.labels === 'boolean') {
+ chartSpec = {
+ ...chartSpec,
+ labels: bp.labels
+ ? { density: 'auto', format: '', prefix: '' }
+ : { density: 'none', format: '', prefix: '' },
+ };
+ } else {
+ chartSpec = {
+ ...chartSpec,
+ labels: {
+ ...chartSpec.labels,
+ ...(bp.labels as NormalizedChartSpec['labels']),
+ },
+ };
+ }
+ }
+ if (bp.legend) {
+ chartSpec = {
+ ...chartSpec,
+ legend: {
+ ...chartSpec.legend,
+ ...(bp.legend as NormalizedChartSpec['legend']),
+ },
+ };
+ }
+ if (bp.annotations) {
+ const annotationWarnings: string[] = [];
+ chartSpec = {
+ ...chartSpec,
+ annotations: normalizeAnnotations(bp.annotations, annotationWarnings),
+ };
+ emitSpecWarnings(annotationWarnings, options.onWarn);
+ // User explicitly provided annotations at this breakpoint — override the
+ // responsive strategy so they render inline instead of being stripped.
+ annotationsInline = true;
+ // The default compact limit would demote the annotations the author just
+ // wrote for this breakpoint; only an explicit limit still applies.
+ const authoredLimit =
+ typeof rawSpec.responsive === 'object' &&
+ rawSpec.responsive !== null &&
+ (rawSpec.responsive as Record).compactAnnotationLimit !== undefined;
+ if (!authoredLimit) chartSpec = { ...chartSpec, compactAnnotationLimit: null };
+ }
+ // New override branches for sparkline mode and related fields:
+ if (bp.display !== undefined) {
+ chartSpec = {
+ ...chartSpec,
+ display: bp.display as NormalizedChartSpec['display'],
+ };
+ }
+ if (bp.encoding !== undefined) {
+ // Merge encoding so a breakpoint can flip on/off encoding.x.axis or
+ // encoding.y.axis (used by sparkline display mode to opt back in to
+ // axes at a specific breakpoint).
+ chartSpec = {
+ ...chartSpec,
+ encoding: mergeEncoding(
+ chartSpec.encoding,
+ bp.encoding as Record