facebook / facebook/astryx

feat: charts — compositional d3 chart system (@astryxdesign/charts) + declarative Vega wrapper (@astryxdesign/vega)

Open
#977 15 comments 0 reactions 1 assignee Claimed by @cixzhang View on GitHub
component enhancement
Dominant language
TypeScript
Stars
13k
Forks
1.1k
Avg merge
1d 15h
Merged PRs (30d)
690

Description

## Context

> **Updated July 2026 — strategy has changed substantially since this issue was filed.** The original plan (wrap a third-party Recharts-based chart lib with thin styled components in `@astryxdesign/core`) has been dropped. Charts are now a **compositional, d3-backed system of their own**, split across **two dedicated packages**. The canonical write-up of the architecture is the [Chart System Architecture](https://github.com/facebook/astryx/wiki/Chart-System-Architecture) wiki page — this issue tracks the remaining work to graduate it into published packages.

### Two packages

**1. `@astryxdesign/charts` — compositional d3 chart primitives**

The foundation. Bundles d3 (`d3-scale`, `d3-shape`, `d3-array`, `d3-path`) as a real dependency and builds our own React SVG/WebGL components on top — no intermediate charting library.

The design is **compositional**:

- **Chart containers** own the coordinate space. A container computes the scales from data and exposes them via React context; it holds the refs and dimensions for one chart. Different chart containers exist per chart type, each with their own references and dimensions.
- **Mark modules plug into a container** to render shapes. Bars, lines, areas, dots, error bars, candlesticks, etc. are independent sibling components that read the container's scales from context. Because the only scale available is the container's, a mark physically cannot render at a different scale than its siblings — axes and marks can't disagree (Tier 1 correctness guarantees; see the wiki).
- New chart types are added by composing a container with the mark modules it needs, rather than configuring a monolith.

This gives us full control over the API surface, styling (StyleX + design tokens), and interactivity (brush, zoom, crosshair, streaming, GPU hit-testing).

**2. `@astryxdesign/vega` — declarative, spec-based charts**

A Vega-Lite wrapper for the declarative use case. Depends on **both `@astryxdesign/charts` and Vega/Vega-Lite** — it draws its palette from the same `@astryxdesign/core` color system as the compositional charts (so the two stay visually in sync), and depends on `charts` to plug into shared chart behavior like cross-chart interaction coordination — while letting consumers describe charts as JSON specs.

- Good for AI-generated charts, exploratory/analytical dashboards, and spec-as-config (store a chart definition as JSON, render it themed).
- **Caveat:** some interactivity is harder to implement in the spec-based model than in the compositional one. Vega owns its own scene graph, so fine-grained interaction, custom hit-testing, and WebGL/streaming paths that `@astryxdesign/charts` offers natively are limited or unavailable here.

### Why two libraries

They serve two genuinely different authoring models, and neither subsumes the other:

- **Composition (`charts`)** is imperative-in-React: you assemble a container + marks + interaction layers. Maximum control, maximum interactivity, correctness enforced by the shared-scale architecture. This is the primary, batteries-included path for product surfaces.
- **Declarative (`vega`)** is spec-first: you hand over JSON and get a chart. Far more concise for standard chart types, serializable/portable, and ideal for LLM- or query-generated visualizations — at the cost of interaction depth.

The **color system and token resolution live in `@astryxdesign/core`**, and both packages consume it from there — so the compositional charts and the Vega wrapper look identical across themes and modes without either owning the palette. `vega` still depends on `charts` (not the reverse) because charts is the home for cross-cutting chart behavior — e.g. **cross-chart interaction coordination** (linked selections/brushing/hover across multiple charts), which will likely live in the charts library and which the Vega wrapper should be able to participate in. Rather than force every consumer into one paradigm, we ship both, keep them visually unified through core, and let the declarative layer build on the compositional one.

This also **sets the precedent for future integration libraries**. `@astryxdesign/vega` is the first example of the pattern — an integration package that pulls its palette from the core color system and plugs into shared chart behavior in `charts`, wrapping a third-party rendering engine. If we later want first-class support for another charting library (a different declarative grammar, a high-performance canvas/WebGL time-series lib, etc.), it follows the same shape: a thin `@astryxdesign/{engine}` package that consumes core tokens and depends on `charts` for cross-cutting behavior, without any consumer having to abandon the compositional or declarative paths they already use.

### Design tokens & color system (shipped)

Chart colors live in `@astryxdesign/core` as **data-visualization tokens** under the `--color-data-*` namespace (`packages/core/src/theme/domainTokens/dataTokens.ts`). They are theme-aware (`light-dark()` values) and shared by both packages, so the compositional charts and the Vega wrapper render identically across themes and modes.

**Categorical** — 10 named accent colors for distinct series/dimensions:

```
--color-data-categorical-blue --color-data-categorical-red
--color-data-categorical-orange --color-data-categorical-teal
--color-data-categorical-purple --color-data-categorical-brown
--color-data-categorical-green --color-data-categorical-indigo
--color-data-categorical-pink --color-data-categorical-cyan
```

**Neutral** — a single neutral tone for labels, reference lines, empty states:

```
--color-data-neutral
```

**Sequential ramps** — 9 hues × 5 steps (`5` = darkest → `1` = lightest), for ordered/quantitative scales, heatmaps, choropleths:

```
--color-data-{hue}-{1..5}
hue ∈ blue · shamrock · orange · pink · purple · red · teal · yellow · gray
```

Consumers don't hand-wire tokens — they go through the color API, which resolves tokens to values (mode-aware, no `getComputedStyle` round-trip):

```tsx
const colors = useChartColors(); // React
// const colors = getChartColors(theme, mode); // non-React, same API

colors.categorical(5) // 5 distinct series colors
colors.sequential.blue(3) // 3-stop blue ramp (dark → light)
colors.diverging.positiveNegative(7) // shamrock → gray-1 → red
colors.diverging.coldHot(7)
colors.diverging.custom('purple', 'orange', 7)
colors.semantic.positive // categorical-green
colors.semantic.negative // categorical-red
colors.semantic.warning // categorical-orange
colors.semantic.neutral // color-data-neutral
colors.structural.axis / .grid / .tick / .label // chart chrome, from core border/text tokens
colors.alpha('#0171E3', 0.5) // any color with opacity
```

Sequential ramps are perceptually ordered (dark = high). Diverging palettes have a neutral midpoint (`--color-data-gray-1`). Legends accept palette output directly. Full rationale — the perceptual/contrast/CVD analysis that produced these values, and why we abandoned both the `--xds-data-viz-*` www taxonomy and the ordinal `--chart-N` scheme — is preserved in the comments below.

### Current status

- The compositional chart components (container + marks + interaction layers + WebGL/streaming + color hook) are incubating in `packages/lab` (`Chart`, `ChartBar`, `ChartLine`, `ChartAxis`, `useChartColors`/`getChartColors`, …).
- The `--color-data-*` tokens and color API are shipped in `@astryxdesign/core`.
- `@astryxdesign/vega` exists as a package (Vega-Lite wrapper, Vega/Vega-Lite as peer deps).
- **Remaining work tracked by this issue:** graduate the lab chart system into a published `@astryxdesign/charts` package with d3 as a bundled dependency, and wire `@astryxdesign/vega` to depend on it for shared theming.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.