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
4 changes: 3 additions & 1 deletion .claude/rules/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ node scripts/release.mjs 6.15.0 # explicit version

The script:
1. Bumps `version` in all 6 `packages/*/package.json` files
2. Updates `CHANGELOG.md` in each package from conventional commits
2. Copies the reviewed version entry from the root `CHANGELOG.md` into each package changelog; falls back to conventional commits when no reviewed entry exists
3. Commits as `release: openchart vX.Y.Z`
4. Creates per-package git tags via GitHub API (`core-vX.Y.Z`, `engine-vX.Y.Z`, etc.) so they get verified signatures
5. Pushes the commit to origin
Expand Down Expand Up @@ -57,6 +57,8 @@ working tree is unchanged.

CI and deploy-examples skip on release commits (prefix `release:`). The Release workflow handles build/typecheck/test verification before publishing.

Before releasing, write the target version section in the root changelog and review it in the release-preparation PR. Run `bun run test:scripts` to verify dry-run behavior and changelog selection.

## Choosing bump type

- `patch` for bug fixes only
Expand Down
2 changes: 1 addition & 1 deletion .claude/rules/svg-animation.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,6 @@ Data-update transitions live in `packages/vanilla/src/transition/` (barrel: `tra

**Never-tween-opacity-on-updates rule.** Updated marks (same key in prev and next) should NOT have their opacity tweened. The renderer may have set `opacity="0"` on points suppressed under endpoint markers. Tweening opacity would make those points flash visible during the transition. Only enter/exit tweens use opacity.

**Path-morph freeze-and-crossfade on interruption.** When `.update()` is called while a line/area series is mid-morph, the interpolated point array matches neither layout's data. Re-matching is intractable. Instead: `snapshot()` captures the current interpolated path `d` string, creates a ghost carrying that frozen path, crossfades to the new final path. Rects/points retarget smoothly from their current interpolated position.
**Path-morph interruption.** Line snapshots retain sampled points and point keys, so the next update retargets from visible geometry when correspondence is available. Area snapshots (and line snapshots without point correspondence) retain the current path as a frozen ghost and crossfade to the new final path. Rects and points retarget from their sampled position. Preserve missing-observation gaps in both paths.

**Arc transitions.** Pie/donut slices tween angles and radii (plus the pie center), rebuilding the path per frame via the engine's `buildArcPath` — geometry-based, so interruption retargets like rects (the snapshot stores angles/radii/center; no freeze-and-crossfade needed). Snap-to-final writes the engine's exact `mark.path`. Exiting slices ghost and collapse to their midpoint angle while fading. Sparklines are no longer excluded from transitions — a live-updating sparkline is the canonical real-time tile.
45 changes: 5 additions & 40 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,17 @@ jobs:
LINT_PID=$!
bun run build || { echo "::error::Build failed"; exit 1; }
wait $LINT_PID || { echo "::error::Lint failed"; exit 1; }
bun run check:boundaries || exit 1
bun run check:library-bundle || exit 1
bun run typecheck || { echo "::error::Typecheck failed"; exit 1; }
# Fail if spec.ts changed without regenerating the schema / llms.txt
# (runs after build: generate:llms imports the built core).
bun run check:generated || { echo "::error::Generated schema or llms.txt is stale. Run: bun run generate"; exit 1; }

- name: Test
run: bun run test
run: |
bun run test
bun run test:scripts

invariants:
name: Playwright layout invariants
Expand Down Expand Up @@ -85,42 +89,3 @@ jobs:
name: playwright-report
path: playwright-report/
retention-days: 7

workbench:
name: Workbench production browser QA
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup
uses: ./.github/actions/setup-bun
- name: Install browsers
run: bunx playwright install --with-deps chromium webkit
- name: Test production artifact
run: |
bun run check:boundaries
bun run build
bun run --cwd apps/studio typecheck
bun run --cwd apps/studio lint
bun run --cwd apps/studio test
bun run --cwd apps/studio test:e2e
bun run check:library-bundle
- name: Verify independent checkout and nested hosting
run: |
bun run extract:studio "$RUNNER_TEMP/OpenChart Studio"
cd "$RUNNER_TEMP/OpenChart Studio"
STUDIO_TEST_BASE_PATH=/tools/studio/ bun run test:e2e
- name: Upload browser evidence
if: failure()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: workbench-browser-evidence
path: |
apps/studio/test-results/
${{ runner.temp }}/OpenChart Studio/test-results/
retention-days: 7
4 changes: 3 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ jobs:
run: bun run typecheck

- name: Test
run: bun run test
run: |
bun run test
bun run test:scripts

- name: Extract version from commit message
id: version
Expand Down
34 changes: 32 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this

## [Unreleased]

## [8.9.0] - 2026-10-14

### Added

- **authoring APIs:** field metadata, structured design diagnostics, deterministic composition suggestions, guarded spec patches, observation comparisons and explicit dataset bindings.
- **reader:** a framework-independent reader controller with preview/pins and sampled geometry, plus persistent observation identities.
- **motion:** opt-in `fluid`, `subtle` and `legacy` timing presets; bounded spring motion; compatible SVG scene retention and coordinated label transitions.
- **responsive:** final-tick preservation, wrapped endpoint summaries, growing chrome and priority-column mobile tables. Added the opt-in `clarity` theme and `legacyEditorial` preset.

### Fixed

- **library:** rejected updates preserve the last valid chart/table; animated maps, tile maps and tables resume resizing after entrance completion. Sankey updates no longer replay entrances, and disabling tooltips no longer disables callbacks.
Expand All @@ -15,13 +24,34 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
- **export:** specialized SVG/raster exports render settled geometry without transient hover effects; maps preserve the current camera and clipping. Font embedding respects inline typography and inherited span fonts. Table CSV includes every filtered/sorted row across pagination.
- **typography:** bar lists and Sankey use shared chrome wrapping and measured font styles, keeping headings inside narrow containers.

- **rendering:** retained canvas charts keep axes and annotations above the canvas after updates and resizing; donut labels follow their slice when the center moves.
- **reader:** initial Left/Up navigation starts at the final observation; group links accept ordinary labels such as `constructor`.
- **scales:** continuous legends, layered domains and map point palettes now agree with the measurements and classes used to draw marks. Window/density grouping preserves typed keys, and cumulative sums skip booleans.
- **diagnostics:** `onDiagnostic` accepts inline `data.values` and transformed fields while keeping fixes guarded against the authored document.
- **motion:** physically equivalent spring parameters remain finite even at very large or small scales.

### Changed

- **graph!:** `highlight()` now layers over the sticky category filter instead of replacing it. In v8.0 the two shared one slot and the last writer won, so hovering a custom legend row threw away the filter the reader had just clicked. The effective emphasis is now the intersection of filter and highlight, except that a highlight disjoint from the filter previews on its own and a highlight matching no nodes is a no-op layer. Two consequences: `clearHighlight()` releases only the transient highlight and returns to the filtered view (clearing the filter is `setActiveCategories([])`), and `onHighlightChange` reports the effective set, so it can fire with an array where it previously fired `null`. Legend `active` flags now track the filter alone. See [migration guide](docs/migrating-v8.md#24-graph-highlight-layers-over-the-category-filter-81).
- **Studio:** the standalone authoring app moved to [OpenStudio](https://github.com/tryopendata/openstudio). OpenChart ships the reusable visualization library and public authoring APIs; Studio UI, dependencies, persistence and hosting live in their own repository.
- **tooling:** development uses Node.js 24, TypeScript 7 and Vitest 5, with the TypeScript 6 compatibility API retained for build tooling. React and optional Three.js development dependencies were updated. Published package runtime requirements are unchanged.

### Features
## [8.8.0] - 2026-09-28

### Added

- **map:** `geo.zoom` lets readers zoom and pan a GeoMap. Opt-in (`zoom: true`, or `{ maxZoom?: number, controls?: boolean }`), so existing maps are unchanged. Ctrl/cmd + wheel and trackpad pinch zoom around the cursor while a plain wheel still scrolls the page (with a brief hint), drag pans, double-click zooms, two-finger pinch zooms on touch (one finger pans only once zoomed in), and `+`/`-`/arrows/`0` work when the map has focus. On-screen zoom in / zoom out / reset buttons follow the theme in light and dark. Zoom is clamped to 1x..`maxZoom` (default 12), panning can't leave the frame, points keep their screen size, and gestures share one camera with `zoomTo`/`setCamera`/focus. New `onCameraChange` mount option (and `<GeoMap onCameraChange>` / `camera-change` / `oncamerachange` in React, Vue, Svelte).
- **docs:** Built for Agents examples and walkthrough.

Releases 8.1.1 through 8.7.0 are recorded in the [package changelog](packages/core/CHANGELOG.md).

## [8.1.0] - 2026-08-15

### Changed

- **graph!:** `highlight()` now layers over the sticky category filter instead of replacing it. In v8.0 the two shared one slot and the last writer won, so hovering a custom legend row threw away the filter the reader had just clicked. The effective emphasis is now the intersection of filter and highlight, except that a highlight disjoint from the filter previews on its own and a highlight matching no nodes is a no-op layer. Two consequences: `clearHighlight()` releases only the transient highlight and returns to the filtered view (clearing the filter is `setActiveCategories([])`), and `onHighlightChange` reports the effective set, so it can fire with an array where it previously fired `null`. Legend `active` flags now track the filter alone. See [migration guide](docs/migrating-v8.md#24-graph-highlight-layers-over-the-category-filter-81).

### Added

- **graph:** `seedNode` on `GraphSpec` — name the graph's seed/root node (`seedNode: 'id'`, or `{ id, style }`) and it gets a ring in the theme text color, a 2px stroke, and an always-on label, plus exemption from focus dimming (highlight, legend filter, hover, and selection; search still dims it). Only the seed is exempt; its neighbors dim normally, so a hub seed doesn't light half the graph. Radius stays with the `nodeSize` encoding unless `style.radius` is set. `style` layers under any `nodeOverrides` entry for the same id, and an id matching no node warns rather than throwing. Unrelated to `layout.seed`.
- **graph:** `legend: false` set on the spec (rather than as a mount option) now actually suppresses the built-in legend. The field was typed and documented but only the mount option was read, so a spec-only host got two legends. The mount option still wins when both are set.
- **graph:** `onWarn` mount option routes compiler advisories (unknown `seedNode` id, deprecations) somewhere other than `console.warn`. Each distinct message is emitted at most once per instance, so a standing warning doesn't repeat on every `update()`.
Expand Down
34 changes: 15 additions & 19 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,14 @@ it only changes bundled type-checking sources, not Ladle's runtime.
```bash
bun run dev # Start the examples dev server (Ladle) at localhost:6006
bun run build # Build all packages in dependency order
bun run test # Run all tests
bun run test # Run library and example tests
bun run test:scripts # Test release tooling
bun run typecheck # Type-check all packages
bun run lint # Lint all packages
```

The browser authoring app lives in [OpenStudio](https://github.com/tryopendata/openstudio), with its own dependencies, tests and GitHub Pages deployment.

The dev server uses [Ladle](https://ladle.dev) to render stories from `examples/src/`. Each story is a standalone React component that demonstrates a feature. Open it in a browser to see your changes live.

## Project structure
Expand All @@ -44,11 +47,13 @@ openchart/
engine/ Headless compiler: spec in, layout out
vanilla/ DOM rendering: SVG charts, HTML tables
react/ React components wrapping vanilla
vue/ Vue 3 components wrapping vanilla
svelte/ Svelte 5 components wrapping vanilla
examples/ Ladle stories for visual development
docs/ Documentation
```

Dependency direction: `core <- engine <- vanilla <- react`. Never import upstream.
Dependency direction: `core <- engine <- vanilla <- react / vue / svelte`. Never import upstream.

For a deeper look at how the packages fit together, see [docs/architecture.md](docs/architecture.md).

Expand Down Expand Up @@ -111,18 +116,17 @@ export function computeMyTypeLabels(

### Step 4: Register the renderer

`index.ts` composes compute + labels into a `ChartRenderer` and registers it:
`index.ts` composes compute + labels into an exported `ChartRenderer`:

```ts
// packages/engine/src/charts/<type>/index.ts

import type { Mark } from "@opendata-ai/openchart-core";
import { registerChartRenderer } from "../registry";
import type { ChartRenderer } from "../registry";
import { computeMyTypeMarks } from "./compute";
import { computeMyTypeLabels } from "./labels";

const myTypeRenderer: ChartRenderer = (spec, scales, chartArea, strategy) => {
export const myTypeRenderer: ChartRenderer = (spec, scales, chartArea, strategy) => {
const marks = computeMyTypeMarks(spec, scales, chartArea, strategy);
const labels = computeMyTypeLabels(marks, chartArea);
for (let i = 0; i < marks.length && i < labels.length; i++) {
Expand All @@ -131,26 +135,18 @@ const myTypeRenderer: ChartRenderer = (spec, scales, chartArea, strategy) => {
return marks as Mark[];
};

registerChartRenderer("<type>", myTypeRenderer);

export { computeMyTypeMarks } from "./compute";
export { computeMyTypeLabels } from "./labels";
```

### Step 5: Wire it up

1. Add the import to `packages/engine/src/index.ts`:

```ts
import "./charts/<type>/index";
```

This import triggers self-registration at module load time.
1. Import `myTypeRenderer` in `packages/engine/src/charts/builtin.ts` and add `registerChartRenderer("<type>", myTypeRenderer)` inside `registerBuiltinRenderers()`. Keeping registration there also lets tests restore built-ins after clearing the registry.

2. Add the type string to the `ChartType` union in `packages/core/src/types/spec.ts`:
2. Add the type string to the `MarkType` union in `packages/core/src/types/spec.ts`:

```ts
export type ChartType = 'line' | 'area' | 'bar' | ... | '<type>';
export type MarkType = 'line' | 'area' | 'bar' | ... | '<type>';
```

3. Add encoding rules in `packages/core/src/types/encoding.ts` (which channels are required/optional for this type).
Expand Down Expand Up @@ -266,7 +262,7 @@ import { describe, it, expect } from "vitest";
// Fixture factories at the top of the file
function createTestSpec(): ChartSpec {
return {
type: "line",
mark: "line",
data: [{ x: 1, y: 2 }],
encoding: {
x: { field: "x", type: "quantitative" },
Expand All @@ -287,7 +283,7 @@ Run tests from the repo root:

```bash
bun run test # All tests
bun run test -- --watch # Watch mode
bunx vitest --watch # Watch mode
bun run test -- path/to/file # Single file
```

Expand All @@ -313,7 +309,7 @@ node scripts/release.mjs 8.1.0-rc.1 # explicit version
node scripts/release.mjs minor --dry-run # preview, writes nothing
```

The script bumps every `packages/*/package.json`, generates changelogs from conventional commits, commits as `release: openchart vX.Y.Z`, creates per-package tags (`core-vX.Y.Z`, ...) via the GitHub API, and pushes.
The script bumps every `packages/*/package.json`, copies the reviewed version entry from the root changelog (or generates notes from conventional commits when no entry exists), commits as `release: openchart vX.Y.Z`, creates per-package tags (`core-vX.Y.Z`, ...) via the GitHub API, and pushes.

Don't edit `package.json` versions by hand and don't create release tags yourself. Pushing the `release:` commit triggers the Release workflow, which builds, tests, rewrites `workspace:*` dependencies to real versions, and publishes all six packages to npm. Prerelease versions (anything with a `-`) publish under the `next` dist-tag, so they never take `latest`.

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ Release candidates publish under the `next` dist-tag, so a plain install always

| Doc | What's in it |
| --- | ------------ |
| [Studio workbench](docs/workbench.md) | Build and deploy the portable authoring application |
| [OpenStudio](https://github.com/tryopendata/openstudio) | Separate browser authoring app and GitHub Pages demo |
| [Getting started](docs/getting-started.md) | Hands-on tutorial from first chart to custom themes, tables, and graphs |
| [Chart types](docs/chart-types.md) | Catalog of every mark and spec type, with when to reach for each |
| [Spec reference](docs/spec-reference.md) | Field-by-field type reference for ChartSpec, TableSpec, and GraphSpec |
Expand Down
5 changes: 0 additions & 5 deletions apps/studio/.gitignore

This file was deleted.

Loading
Loading