|
| 1 | +# agents-chart |
| 2 | + |
| 3 | +A semantic-level visualization library that compiles data + semantic annotations |
| 4 | +into chart specifications for multiple rendering backends. The LLM outputs only |
| 5 | +chart type, field assignments, and a **semantic type** per field (e.g. `Revenue`, |
| 6 | +`Rank`, `CategoryCode`). A deterministic compiler derives all low-level |
| 7 | +parameters — sizing, zero-baseline, formatting, color schemes, and mark |
| 8 | +templates — so charts look good *and* stay editable without calling the LLM again. |
| 9 | + |
| 10 | +Pure TypeScript · No UI framework dependencies · Data-in, spec-out |
| 11 | + |
| 12 | +> For full motivation & comparisons, see [docs/story.md](docs/story.md). |
| 13 | +> For architecture details, see [docs/design_v3.md](docs/design_v3.md). |
| 14 | +
|
| 15 | +--- |
| 16 | + |
| 17 | +## Why |
| 18 | + |
| 19 | +LLM-generated chart specs face a dilemma: |
| 20 | + |
| 21 | +| Approach | Looks good | Editable | Bespoke charts | Cost to re-encode | |
| 22 | +|----------|:---:|:---:|:---:|:---:| |
| 23 | +| Library defaults | ✗ | ✓ | ✗ | 0 | |
| 24 | +| LLM-tuned spec | ✓ | ✗ | Sometimes | 1 LLM call | |
| 25 | +| **agents-chart** | **✓** | **✓** | **✓** | **0** | |
| 26 | + |
| 27 | +**Simple specs** are editable but look bad (wrong sizing, misleading |
| 28 | +encodings). **Polished specs** look great but are brittle (hard-coded |
| 29 | +values break on every field swap). agents-chart resolves this: when a user |
| 30 | +swaps fields, changes chart type, or adds facets for exploration, the |
| 31 | +compiler re-derives all parameters automatically — no LLM call needed. |
| 32 | + |
| 33 | +Because the output is native library code (Vega-Lite, ECharts, or Chart.js), |
| 34 | +users retain full control over aesthetic fine-tuning — fonts, colors, legends, |
| 35 | +annotations — using each library's own API. There is no abstraction tax or |
| 36 | +reduced expressiveness. |
| 37 | + |
| 38 | +### Key insight: semantic types as the contract |
| 39 | + |
| 40 | +Instead of asking the LLM to set dozens of low-level parameters, we ask |
| 41 | +it one thing: **what does this data mean?** — expressed as a semantic type. |
| 42 | + |
| 43 | +``` |
| 44 | +Semantic type (e.g. "Revenue") |
| 45 | + ├── Encoding type: quantitative |
| 46 | + ├── Zero baseline: true |
| 47 | + ├── Domain padding: 0% |
| 48 | + ├── Scale direction: normal |
| 49 | + ├── Axis formatting: "$,.0f" |
| 50 | + ├── Color scheme: sequential |
| 51 | + └── Sizing model: per-axis stretch |
| 52 | +``` |
| 53 | + |
| 54 | +When the user swaps a field, the compiler re-derives everything from the new |
| 55 | +semantic type. No hard-coded constants go stale. No LLM call needed. |
| 56 | + |
| 57 | +### The workflow |
| 58 | + |
| 59 | +``` |
| 60 | +1. LLM generates: chart type + semantic types (~10-line JSON) |
| 61 | +2. User edits: swap field / change mark / add facet → compiler handles it (no AI) |
| 62 | +3. Fine-tune (2%): edit the generated spec directly for bespoke styling |
| 63 | +``` |
| 64 | + |
| 65 | +--- |
| 66 | + |
| 67 | +## Quick start |
| 68 | + |
| 69 | +### Vega-Lite |
| 70 | + |
| 71 | +```ts |
| 72 | +import { assembleVegaLite } from './lib/agents-chart'; |
| 73 | + |
| 74 | +const spec = assembleVegaLite({ |
| 75 | + data: { values: myData }, |
| 76 | + semantic_types: { weight: 'Quantity', mpg: 'Quantity', origin: 'Country' }, |
| 77 | + chart_spec: { |
| 78 | + chartType: 'Scatter Plot', |
| 79 | + encodings: { x: { field: 'weight' }, y: { field: 'mpg' }, color: { field: 'origin' } }, |
| 80 | + canvasSize: { width: 400, height: 300 }, |
| 81 | + }, |
| 82 | +}); |
| 83 | +``` |
| 84 | + |
| 85 | +### ECharts |
| 86 | + |
| 87 | +```ts |
| 88 | +import { assembleECharts } from './lib/agents-chart'; |
| 89 | + |
| 90 | +const option = assembleECharts({ |
| 91 | + data: { values: myData }, |
| 92 | + semantic_types: { weight: 'Quantity', mpg: 'Quantity' }, |
| 93 | + chart_spec: { |
| 94 | + chartType: 'Scatter Plot', |
| 95 | + encodings: { x: { field: 'weight' }, y: { field: 'mpg' } }, |
| 96 | + }, |
| 97 | +}); |
| 98 | +``` |
| 99 | + |
| 100 | +### Chart.js |
| 101 | + |
| 102 | +```ts |
| 103 | +import { assembleChartjs } from './lib/agents-chart'; |
| 104 | + |
| 105 | +const config = assembleChartjs({ |
| 106 | + data: { values: myData }, |
| 107 | + semantic_types: { weight: 'Quantity' }, |
| 108 | + chart_spec: { chartType: 'Bar Chart', encodings: { x: { field: 'category' }, y: { field: 'value' } } }, |
| 109 | +}); |
| 110 | +``` |
| 111 | + |
| 112 | +--- |
| 113 | + |
| 114 | +## Architecture |
| 115 | + |
| 116 | +``` |
| 117 | +index.ts ← public API (re-exports core/ + all backends) |
| 118 | +
|
| 119 | +core/ ← target-language-agnostic |
| 120 | + types.ts ← shared type definitions (ChartAssemblyInput, ChartTemplateDef, …) |
| 121 | + semantic-types.ts ← ~70 semantic types + VisCategory helpers |
| 122 | + decisions.ts ← pure decision functions (layout, encoding type) |
| 123 | + resolve-semantics.ts ← Phase 0: semantic resolution |
| 124 | + compute-layout.ts ← Phase 1: layout computation |
| 125 | + filter-overflow.ts ← overflow filtering |
| 126 | +
|
| 127 | +vegalite/ ← Vega-Lite backend |
| 128 | + assemble.ts ← assembleVegaLite() orchestrator |
| 129 | + instantiate-spec.ts ← Phase 2: VL spec instantiation |
| 130 | + templates/ ← chart templates (bar, scatter, bump, …) |
| 131 | +
|
| 132 | +echarts/ ← ECharts backend |
| 133 | + assemble.ts ← assembleECharts() orchestrator |
| 134 | + instantiate-spec.ts ← Phase 2: EC option instantiation |
| 135 | + templates/ ← chart templates |
| 136 | +
|
| 137 | +chartjs/ ← Chart.js backend |
| 138 | + assemble.ts ← assembleChartjs() orchestrator |
| 139 | + instantiate-spec.ts ← Phase 2: CJS config instantiation |
| 140 | + templates/ ← chart templates |
| 141 | +``` |
| 142 | + |
| 143 | +### Type resolution pipeline |
| 144 | + |
| 145 | +``` |
| 146 | + semantic type → getVisCategory() → VisCategory → channel/chart rules → encoding type |
| 147 | + ↑ |
| 148 | + (fallback: inferVisCategory() inspects raw data) |
| 149 | +``` |
| 150 | + |
| 151 | +--- |
| 152 | + |
| 153 | +## Public API |
| 154 | + |
| 155 | +### Assembly functions |
| 156 | + |
| 157 | +Each backend has its own assembly function. All accept the same |
| 158 | +`ChartAssemblyInput` shape: |
| 159 | + |
| 160 | +| Function | Output | Import | |
| 161 | +|----------|--------|--------| |
| 162 | +| `assembleVegaLite(input)` | Vega-Lite spec | `import { assembleVegaLite } from './lib/agents-chart'` | |
| 163 | +| `assembleECharts(input)` | ECharts option object | `import { assembleECharts } from './lib/agents-chart'` | |
| 164 | +| `assembleChartjs(input)` | Chart.js config object | `import { assembleChartjs } from './lib/agents-chart'` | |
| 165 | + |
| 166 | +### Input types |
| 167 | + |
| 168 | +```ts |
| 169 | +interface ChartAssemblyInput { |
| 170 | + data: { values: any[] } | { url: string }; // inline rows or URL |
| 171 | + semantic_types?: Record<string, string>; // field → semantic type |
| 172 | + chart_spec: { |
| 173 | + chartType: string; // e.g. "Scatter Plot" |
| 174 | + encodings: Record<string, ChartEncoding>; // channel → encoding map |
| 175 | + canvasSize?: { width: number; height: number }; // default 400×320 |
| 176 | + chartProperties?: Record<string, any>; // template-specific knobs |
| 177 | + }; |
| 178 | + options?: AssembleOptions; // layout tuning |
| 179 | +} |
| 180 | +``` |
| 181 | + |
| 182 | +| Key | Description | |
| 183 | +|---|---| |
| 184 | +| `data` | Data source — either `{ values: [...] }` (inline row objects) or `{ url: "..." }` (JSON/CSV URL) | |
| 185 | +| `semantic_types` | Per-column semantic annotations (e.g., `{ revenue: "Price", country: "Country" }`) | |
| 186 | +| `chart_spec` | What to draw — chart type, encodings, canvas size, properties | |
| 187 | +| `options` | Layout tuning (elasticity, step sizes, tooltips, etc.) | |
| 188 | + |
| 189 | +```ts |
| 190 | +interface ChartEncoding { |
| 191 | + field?: string; |
| 192 | + type?: 'quantitative' | 'nominal' | 'ordinal' | 'temporal'; |
| 193 | + aggregate?: 'count' | 'sum' | 'average'; |
| 194 | + sortOrder?: 'ascending' | 'descending'; |
| 195 | + sortBy?: string; |
| 196 | + scheme?: string; |
| 197 | +} |
| 198 | + |
| 199 | +interface AssembleOptions { |
| 200 | + addTooltips?: boolean; // default false |
| 201 | + elasticity?: number; // axis stretch exponent (default 0.5) |
| 202 | + maxStretch?: number; // axis stretch cap (default 2) |
| 203 | + facetElasticity?: number; // facet stretch exponent (default 0.3) |
| 204 | + maxStretch?: number; // unified stretch cap (default 2) |
| 205 | + minStep?: number; // min px per discrete tick (default 6) |
| 206 | + minSubplotSize?: number; // min facet subplot px (default 60) |
| 207 | +} |
| 208 | +``` |
| 209 | + |
| 210 | +### Template registries |
| 211 | + |
| 212 | +Each backend has its own set of supported chart types and template |
| 213 | +definitions. Templates are organized by category and can be looked up by |
| 214 | +chart type name. |
| 215 | + |
| 216 | +| Backend | Template map | Flat list | Lookup | Channels | |
| 217 | +|---------|-------------|-----------|--------|----------| |
| 218 | +| Vega-Lite | `vlTemplateDefs` | `vlAllTemplateDefs` | `vlGetTemplateDef(name)` | `vlGetTemplateChannels(name)` | |
| 219 | +| ECharts | `ecTemplateDefs` | `ecAllTemplateDefs` | `ecGetTemplateDef(name)` | `ecGetTemplateChannels(name)` | |
| 220 | +| Chart.js | `cjsTemplateDefs` | `cjsAllTemplateDefs` | `cjsGetTemplateDef(name)` | `cjsGetTemplateChannels(name)` | |
| 221 | + |
| 222 | +```ts |
| 223 | +// Example: list available Vega-Lite chart categories |
| 224 | +import { vlTemplateDefs } from './lib/agents-chart'; |
| 225 | +Object.keys(vlTemplateDefs); // ["Scatter & Point", "Bar", "Line & Area", ...] |
| 226 | + |
| 227 | +// Example: get channels for a specific chart type |
| 228 | +import { vlGetTemplateChannels } from './lib/agents-chart'; |
| 229 | +vlGetTemplateChannels('Scatter Plot'); // ["x", "y", "color", "size", "shape"] |
| 230 | +``` |
| 231 | + |
| 232 | +### Semantic types (~70 types) |
| 233 | + |
| 234 | +| Group | Examples | |
| 235 | +|-------|---------| |
| 236 | +| Temporal | `DateTime`, `Date`, `Year`, `Month` | |
| 237 | +| Measures | `Quantity`, `Count`, `Price`, `Percentage` | |
| 238 | +| Discrete numerics | `Rank`, `Score`, `ID` | |
| 239 | +| Geographic | `Latitude`, `Longitude`, `Country`, `City` | |
| 240 | +| Categorical | `PersonName`, `Company`, `Status`, `Boolean` | |
| 241 | +| Ranges | `Range`, `AgeGroup`, `Bucket` | |
| 242 | +| Fallbacks | `String`, `Number`, `Unknown` | |
| 243 | + |
| 244 | +### Core utilities (shared across backends) |
| 245 | + |
| 246 | +These are re-exported from `core/` and available at the top level: |
| 247 | + |
| 248 | +```ts |
| 249 | +import { |
| 250 | + // Semantic type helpers |
| 251 | + inferVisCategory, // infer VisCategory from raw data |
| 252 | + getVisCategory, // look up VisCategory for a known semantic type |
| 253 | + |
| 254 | + // Shared types |
| 255 | + type ChartAssemblyInput, |
| 256 | + type ChartEncoding, |
| 257 | + type ChartTemplateDef, |
| 258 | + type AssembleOptions, |
| 259 | + type ChartWarning, |
| 260 | + |
| 261 | + // Layout constants |
| 262 | + channels, |
| 263 | + channelGroups, |
| 264 | +} from './lib/agents-chart'; |
| 265 | +``` |
| 266 | + |
| 267 | +--- |
| 268 | + |
| 269 | +## What the compiler handles automatically |
| 270 | + |
| 271 | +- **Sizing** — spring model for discrete axes, pressure model for continuous; |
| 272 | + composable with facets and layers. No more 6400 px charts from 80 × 4 facets. |
| 273 | +- **Zero baseline** — Revenue → include zero; Temperature → don't; Rank → don't. |
| 274 | +- **Scale direction** — Rank → reversed; others → normal. |
| 275 | +- **Formatting** — Revenue → `$,.0f`; Percentage → `.0%`; Year → `%Y`. |
| 276 | +- **Color schemes** — categorical codes → distinct hues; measures → sequential. |
| 277 | +- **Label overflow** — auto-rotation and truncation from count + string lengths. |
| 278 | +- **Bespoke marks** — lollipops, bump charts, candlesticks as single templates. |
| 279 | +- **Semantic validation** — actionable errors before rendering, not after crashing. |
| 280 | + |
| 281 | +## Design principles |
| 282 | + |
| 283 | +1. **No UI dependencies** — pure data-in, spec-out. |
| 284 | +2. **Semantic types drive everything** — the caller annotates fields; the |
| 285 | + compiler derives all config. Fallback: `inferVisCategory()` inspects raw data. |
| 286 | +3. **Callers own the data** — no aggregation transforms applied. |
| 287 | +4. **Layout is configurable** — elastic stretch, facet sizing, step sizes |
| 288 | + exposed in `AssembleOptions`. |
| 289 | +5. **Templates are declarative** — each chart type is a `ChartTemplateDef` |
| 290 | + with a skeleton, channel list, and optional post-processor. |
| 291 | +6. **Backend-agnostic semantics** — the same semantic reasoning targets |
| 292 | + Vega-Lite, ECharts, and Chart.js through separate assembly functions. |
0 commit comments