Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 53 additions & 10 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,58 @@
# Changelog

## Unreleased — 8.10
All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/).

## [Unreleased]

## [8.9.1] - 2026-10-17

### Annotation layout and theme options

- Add opt-in rich callout wrapping (`maxWidth`), `subtitleFontSize`, and compact
callout limits. Omitted fields retain existing output; null disables limits.
- Add reference-label placement at line ends, automatic contained placement, and
reserved outside lanes with wrapping and connectors, including facets and
independent-axis layers.
- Add seven annotation theme color tokens with light/dark authoring support;
explicit annotation ink remains authoritative. Rule/range tokens preserve the
existing compiled colors in both modes.
- Support facet `chromeLayout: 'grow'` and report engine-added height so auto-height
mounts and export count chrome, notes and facet growth once.
- Deprecate both `footnoteBandHeight` helpers in favor of resolved
`layout.chrome.noteBandHeight`, preserving their 8.x compatibility signatures.
- Materialize omitted 8.x defaults in `migrateSpecV8` while preserving authored
theme, responsive and layer ownership. Warn about inapplicable mark options
before shorthand expansion, with exact authored paths and repair guidance.

### Annotation geometry and gestures

- Add optional connector `bend` in `[-1, 1]` and engine-resolved curve control
points. Omitted/zero bend retains the existing curve; other connector types
warn and ignore bend.
- Export frozen annotation editing contexts, move/bend previews, and inverse
label placement from the engine.
- Keep authored zero offsets pinned so a committed placement is not nudged on release.

- Convert the first label drag to an explicit anchor/offset and `responsive: false`
atomically, keeping the data point fixed. Remove placement-dependent right
margin reservation so the plot does not shift when the drag commits.
- Reserve note space from source annotation text, including subtitles and pinned
callouts, so dragging cannot resize the plot or figure. Auto-thinned charts may
retain unused note space. Expose the reserved height as `chrome.noteBandHeight`
so rendering, footer placement and export agree. Exhausted fixed-height budgets
report an actionable `layout.note-capacity` diagnostic.
- Add Alt+arrow (1px) and Alt+Shift+arrow (10px) annotation nudges, batched into
one transaction. Escape, pointer/touch cancellation, width changes and document
replacement cancel active drags without emitting edits.
- Animate label and data-anchor positions independently, rebuilding connectors
from engine geometry while preserving annotation enter fades.
- Make drop-line label and connector offsets round-trip through edit previews.
- Place right-side legends after endpoint labels in the gutter already reserved
for them, preventing column overlap in regular and layered charts.

### Editing transactions and compatibility

- Add atomic `EditTransaction` operations, pure `applyEditTransaction` with undo,
optional revisions, and `onEditTransaction` in vanilla and all three wrappers.
Expand All @@ -15,15 +67,6 @@
legacy APIs. Existing 8.x callbacks and optional-field defaults remain supported.
- Centralize shallow theme merging and remove unused internal compatibility paths.


All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/).

## [Unreleased]

## [8.9.1] - 2026-10-16

### Fixed

- **maps:** automatically sized maps grow around wrapped headings and source notes, preserving room for geography on phones. Explicitly sized hosts keep their height budget. Map headings, legends and sources now share the same responsive gutter.
Expand Down
46 changes: 41 additions & 5 deletions docs/codebase-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,14 @@
| Annotations: top-level compile | `packages/engine/src/annotations/compute.ts` → `computeAnnotations()` |
| Annotations: text resolution (anchor → offset → connector) | `packages/engine/src/annotations/resolve-text.ts` |
| Annotations: refline / range | `packages/engine/src/annotations/resolve-refline.ts`, `resolve-range.ts` |
| Annotations: geometry (text bounds, anchor offsets, ray-box connector exit, connector rebuild, arrowhead points) | `packages/engine/src/annotations/geometry.ts` (`connectorExit`, `refreshConnector`, `computeArrowheadPoints`). No `computeConnectorOrigin` — that's the pre-v8 name. |
| Annotations: geometry (text bounds, anchor offsets, ray-box connector exit, connector rebuild, arrowhead points) | `packages/engine/src/annotations/geometry.ts` (`connectorExit`, `refreshConnector`, `automaticControlPoint`, `computeArrowheadPoints`). No `computeConnectorOrigin` — that's the pre-v8 name. |
| Annotations: `**bold**` span parsing | `packages/engine/src/annotations/rich-text.ts` → `parseAnnotationSpans()`, `RichSpan`, `BOLD_SPAN_FONT_WEIGHT` |
| Annotations: block repositioning (label + subtitle + bounds + connector move as one; auto-placement's `textAnchor` stamp) | `packages/engine/src/annotations/move.ts` → `moveAnnotationTo()`, `moveAnnotationBy()`, `annotationBlockBounds()` |
| Annotations: collision resolution | `packages/engine/src/annotations/collisions.ts` |
| Annotations: chrome guard (labels can't climb into the title block) | `packages/engine/src/annotations/collisions.ts` → `clampAnnotationsToBounds(annotations, svgWidth, svgHeight, measure, topBound)`. `topBound` is the first y a label may occupy; `compile.ts` passes `dims.topPad + dims.chrome.topHeight` (`LayoutDimensions.topPad` in `engine/layout/dimensions.ts`) when there is a chrome block, and the same band is fed to auto-placement as a `'chrome'` `ObstacleKind` (`annotations/placement.ts`). |
| Annotations: auto-thinning (demote callouts → numbered footnotes) | `packages/engine/src/annotations/thinning.ts` → `thinAnnotations()`. Three independent demotion rules: pairwise overlap (padded by `COLLISION_PADDING`), containment (`fitsWithin`, checked against `containerBounds ?? plotArea` — callouts legitimately live in the margin, so only labels escaping the *chart* demote), and a `COVERAGE_BUDGET` cap on the share of *plot* area inline labels may claim. The budget is what thins narrow charts — collision resolution spreads labels apart and clamping tucks them back in, so crowded layouts never trip the first two rules. Pinned (`responsive: false`) labels are exempt from the budget. |
| Annotations: wrapped note lines and reserve counts | `packages/engine/src/annotations/notes.ts` → `wrapAnnotationNotes()`, `annotationNoteLineCount()` (internal). `AnnotationFootnote.lines` carries engine-wrapped display lines to rendering/export. |
| Annotations: wrapped note lines and reserve counts | `packages/engine/src/annotations/notes.ts` → `wrapAnnotationNotes()`, `annotationNoteLineCount()`, `annotationNoteCapacity()` (internal; source capacity including pinned text/subtitles and widest numeric prefix). `AnnotationFootnote.lines` carries engine-wrapped display lines to rendering/export; optional `ResolvedChrome.noteBandHeight` carries the stable source-capacity reserve to footer/brand/export and layered alignment. |
| Annotations: frozen editing geometry | `packages/engine/src/annotations/edit-context.ts` → public `createAnnotationEditContext()`, `previewAnnotationMove()`, `previewConnectorBend()`, `inverseLabelPlacement()`. Uses compiled annotation metadata (scales and measurement stay inside engine); snapshots preserve source/facet identity and clamp bounds. |
| Annotations: data → pixel resolver | `packages/engine/src/annotations/position.ts` |
| Annotations: shared constants (`ANCHOR_OFFSET=28`, `CONNECTOR_STANDOFF=6`, `MIN_CONNECTOR_LENGTH=8`, dot defaults, `LEDE_FONT_WEIGHT`, connector strokes, default font) | `packages/engine/src/annotations/constants.ts` |
| Legend layout (entries, wrapping, positioning, `entryPositions`) | `packages/engine/src/legend/compute.ts`, `wrap.ts`; internal `measureCategoricalLegendContent()` reuses the same measurement for merged layer entries. |
Expand Down Expand Up @@ -259,7 +260,7 @@ The vanilla adapter (`mount.ts`) takes the resulting `ChartLayout` and calls `re
- **Typography:** 13px / weight 400 by default, in `theme.fonts.family` (threaded via `AnnotationContext.fontFamily` from `compile.ts`; `FALLBACK_FONT_FAMILY` covers SSR paths). The **lede rule** (`resolveLedeFontWeight`): a `subtitle` with no explicit `fontWeight` promotes the primary text to `LEDE_FONT_WEIGHT` (700); the subtitle stays 400.
- **Thinning** (`annotations/thinning.ts`): `thinAnnotations(annotations, specs, measure, plotArea?, containerBounds?)` demotes overlapping/overflowing labels to numbered footnotes. Containment is checked against `containerBounds ?? plotArea` — annotations legitimately sit in the margin (above a peak, below a trough), so only labels escaping the *chart* demote. The coverage budget still measures against the plot.
- **`specIndex` — the resolved array is NOT the spec array.** `computeAnnotations` drops any annotation whose position falls outside the scale domain, so `annotations[i]` is not `spec.annotations[i]`. Every resolved annotation carries `specIndex` for the trip back; thinning's priority/`responsive` lookup and the faceted footnote numbering both key on it. Indexing `specs[i]` by resolved position is the bug this exists to prevent — it silently mis-associates annotations, and it only shows up once one of them fails to resolve.
- **Faceted thinning** (`compile.ts`, in `compileFaceted`): faceted charts do *not* reach the single-chart `thinAnnotations` call — `compileChart` branches to `compileFaceted` first, so before this existed `autoThin` on a facet was a silent no-op and labels sprawled across neighbouring panels. Each panel now thins against its own `gridPanel.area`, passed as **both** `plotArea` and `containerBounds` (a panel is its own little chart). Footnotes pool into one figure-level `chrome.footnotes` list; since a spec annotation resolves into *every* panel, numbering is keyed on `specIndex` so a label demoted in all N panels gets one footnote, not N copies. The bottom-margin reserve loop mirrors the single-chart path, `frozenChartArea` guard included.
- **Faceted thinning** (`compile.ts`, in `compileFaceted`): faceted charts do *not* reach the single-chart `thinAnnotations` call — `compileChart` branches to `compileFaceted` first, so before this existed `autoThin` on a facet was a silent no-op and labels sprawled across neighbouring panels. Each panel now thins against its own `gridPanel.area`, passed as **both** `plotArea` and `containerBounds` (a panel is its own little chart). Footnotes pool into one figure-level `chrome.footnotes` list; since a spec annotation resolves into *every* panel, numbering is keyed on `specIndex` so a label demoted in all N panels gets one footnote, not N copies. The shared preflight reserves source note capacity once per annotation, including pinned text and subtitles. `frozenChartArea` prevents leaf recompilation from moving the common layer plot.
- **Shared constants:** `annotations/constants.ts` (`ANCHOR_OFFSET`, `CONNECTOR_STANDOFF`, `MIN_CONNECTOR_LENGTH`, `DEFAULT_DOT_RADIUS`/`_STROKE_WIDTH`, `LEDE_FONT_WEIGHT`, connector strokes, default font sizing, dash pattern, refline/text fills for light/dark).

## Chrome system
Expand Down Expand Up @@ -365,12 +366,12 @@ These are easy mistakes — written down so the next agent doesn't search for th
The browser authoring application lives in the separate [OpenStudio repository](https://github.com/tryopendata/openstudio). Its UI, imports, persistence and deployment are not part of this codebase.


## Track A 8.10 additions
## Track A phase 1 additions (released in 8.9.1)

| Concern | Source / exports |
| --- | --- |
| Atomic authored-spec edits and inverse operations | `packages/core/src/helpers/edit-transactions.ts`: public `applyEditTransaction`, `EditTransactionResult`; `types/events.ts`: `EditOp`, `EditTransaction`, optional revision and callback |
| Authored annotation ownership and legacy event bridge | `packages/vanilla/src/interactions/edit-transactions.ts`: `annotationSource`, `editToTransaction`; mount stamps source paths, renderer stamps facet instances; selection and click events preserve both identities |
| Authored annotation ownership and legacy event bridge | `packages/core/src/helpers/annotation-source.ts`: `annotationSource`; `packages/vanilla/src/interactions/edit-transactions.ts`: `editToTransaction`; mount stamps source paths, renderer stamps facet instances; selection and click events preserve both identities |
| Alias migration with repair paths | `packages/engine/src/migration/v8.ts`: public `migrateSpecV8`, `SpecRepair`; layer migrations reuse `compile/layer.ts:buildPrimarySpec` and `compiler/normalize.ts:flattenLayers` |
| Shared v8 shallow theme precedence | `packages/core/src/theme/merge.ts`: `mergeThemeConfig`; `ThemeBase` is internal, public `Theme` remains deprecated |
| Mount compatibility warnings | `packages/vanilla/src/deprecations.ts`; engine warnings remain in `compile/spec-sugar.ts` |
Expand All @@ -381,3 +382,38 @@ require a `LayoutPlan`. Their old positional/plan-less fallbacks are gone.
Graph scheduler, tween and spatial-index callers import from `motion/`
and `spatial-index.ts`; the `graph/` re-export shims are gone. Map fill
transitions accept `MapTransitionSnapshot` only.

## Track A phase 2 additions (released in 8.9.1)

| Concern | Source / exports |
| --- | --- |
| Frozen compiled annotation editing geometry | `packages/engine/src/annotations/edit-context.ts`: `createAnnotationEditContext`, `previewAnnotationMove`, `previewConnectorBend`, `previewConnectorOffset`, `inverseLabelPlacement`; uses actual compiled annotation objects with private measurement metadata |
| Curve control and per-frame annotation geometry | `annotations/geometry.ts`: `automaticControlPoint`, `refreshConnector`; `annotations/interpolate.ts`: `interpolateAnnotationGeometry`; all exported from engine |
| Cancellable mouse/touch gestures and keyboard batches | `packages/vanilla/src/interactions/drag-handler.ts`, `annotation-nudges.ts`; mount flushes pending edits before gesture starts and retargets after synchronous host updates |
| In-place annotation drawing shared by preview and animation | `packages/vanilla/src/renderers/annotations.ts`: `updateAnnotationGeometry`; transition gate/build/apply consume resolved geometry |
| Cross-browser geometry contract | `e2e/annotation-invariants/`; two dedicated projects; gallery `AnnotationCurves` and `AnnotationGestures` stay outside visual baselines |
| Preview benchmark | `packages/engine/src/annotations/__tests__/preview.bench.ts`; Vitest 5 test-context bench, run with `bunx vitest bench --run` |

The chart compiler places right-side legends after resolving the endpoint column,
using its existing gutter reservation. Independent-layer assembly adds only the
remaining right-axis/merged-column offset; `compile-legend-gutter.test.ts` covers
regular, shared-layer and dual-axis column separation.


## Track A phase 3 annotation layout opt-ins (released in 8.9.1)

| Concern | Location |
|---|---|
| Rich wrapping without changing authored text/transition keys | `packages/engine/src/annotations/wrap-text.ts`: `wrapAnnotationText`; `resolve-text.ts` stores `label.lines`/subtitle lines |
| Opt-in reference end/auto/outside geometry | `packages/engine/src/annotations/reference-layout.ts`: `referenceLabelReserve`, `placeReferenceLabel`; `geometry.ts` retains corridor attachments during rebuilds; `compute.ts` uses per-panel reserved lanes owned by `layout/facet.ts`; `dimensions.ts` and `compile/layer.ts` reserve the gutter |
| Compact priority/source count limit | `annotations/thinning.ts`: optional final `compactLimit`; `compile.ts` gates on outer width and height before coverage/collision thinning |
| Resolved note arithmetic and compatibility exports | `packages/core/src/layout/chrome.ts`: `computeNoteBandHeight` and deprecated `footnoteBandHeight`, exported through core index; vanilla adapter in `renderers/chrome.ts`; renderers/brand consume resolved `noteBandHeight` |
| Exactly-once facet grow metadata | `packages/engine/src/compile.ts` facet assembly and `layout/dimensions.ts` populate optional `chrome.grownHeight`; `packages/vanilla/src/mount.ts` deducts engine-owned growth |
| Authored mark-option warnings | `packages/engine/src/compiler/mark-options.ts`: `collectMarkOptionWarnings`; `compile/spec-sugar.ts` calls it before rewrites; `validate.ts` exposes optional warnings |
| 8.x default materialization | `packages/engine/src/migration/v8.ts`: `migrateSpecV8`; parity fixtures in `examples/src/__tests__/migration-parity.test.ts` |
| Layout opt-in browser fixtures | `examples/src/gallery/features-annotations.demos.ts` and `.stories.tsx`: ReferenceLabels, CrowdedCallouts, NotesSizing; `e2e/annotation-invariants/layout.spec.ts` covers geometry, note counts, heights, SVG/PNG and canvas |

The seven annotation colors are resolved theme fields in `core/src/types/theme.ts`,
registered with dark pairs in `theme/resolve.ts` and `theme/dark-mode.ts`, and mirrored
by `styles/token-definitions.ts` and generated CSS. Existing generic annotation
colors retain their previous uses.
Loading
Loading