@jarenjs/charts 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +293 -0
  2. package/dist/types/component/index.d.ts +95 -0
  3. package/dist/types/core/axis.d.ts +77 -0
  4. package/dist/types/core/cartesian.d.ts +127 -0
  5. package/dist/types/core/chart.d.ts +58 -0
  6. package/dist/types/core/domain.d.ts +96 -0
  7. package/dist/types/core/marks.d.ts +86 -0
  8. package/dist/types/core/palette.d.ts +72 -0
  9. package/dist/types/core/scale.d.ts +59 -0
  10. package/dist/types/core/session.d.ts +85 -0
  11. package/dist/types/core/stream-adapter.d.ts +187 -0
  12. package/dist/types/index.d.ts +35 -0
  13. package/dist/types/transforms/benchmark-adapter.d.ts +169 -0
  14. package/dist/types/transforms/mermaid-adapter.d.ts +30 -0
  15. package/dist/types/types/bar.d.ts +213 -0
  16. package/dist/types/types/boxplot.d.ts +116 -0
  17. package/dist/types/types/candlestick.d.ts +218 -0
  18. package/dist/types/types/gauge.d.ts +68 -0
  19. package/dist/types/types/heatmap.d.ts +104 -0
  20. package/dist/types/types/line.d.ts +272 -0
  21. package/dist/types/types/map.d.ts +137 -0
  22. package/dist/types/types/pie.d.ts +146 -0
  23. package/dist/types/types/radar.d.ts +89 -0
  24. package/dist/types/types/sankey.d.ts +100 -0
  25. package/dist/types/types/scatter.d.ts +80 -0
  26. package/dist/types/types/streamgraph.d.ts +75 -0
  27. package/dist/types/types/treemap.d.ts +118 -0
  28. package/package.json +76 -0
  29. package/schemas/chart-definition.schema.json +448 -0
  30. package/src/component/index.js +125 -0
  31. package/src/core/axis.js +221 -0
  32. package/src/core/cartesian.js +192 -0
  33. package/src/core/chart.js +101 -0
  34. package/src/core/domain.js +123 -0
  35. package/src/core/marks.js +110 -0
  36. package/src/core/palette.js +126 -0
  37. package/src/core/scale.js +106 -0
  38. package/src/core/session.js +0 -0
  39. package/src/core/stream-adapter.js +613 -0
  40. package/src/index.js +40 -0
  41. package/src/transforms/benchmark-adapter.js +298 -0
  42. package/src/transforms/mermaid-adapter.js +19 -0
  43. package/src/types/bar.js +276 -0
  44. package/src/types/boxplot.js +216 -0
  45. package/src/types/candlestick.js +274 -0
  46. package/src/types/gauge.js +140 -0
  47. package/src/types/heatmap.js +176 -0
  48. package/src/types/line.js +349 -0
  49. package/src/types/map.js +378 -0
  50. package/src/types/pie.js +163 -0
  51. package/src/types/radar.js +224 -0
  52. package/src/types/sankey.js +391 -0
  53. package/src/types/scatter.js +148 -0
  54. package/src/types/streamgraph.js +158 -0
  55. package/src/types/treemap.js +322 -0
  56. package/styles/charts.css +83 -0
package/README.md ADDED
@@ -0,0 +1,293 @@
1
+ # @jarenjs/charts
2
+
3
+ **Headless charts** for the jaren suite: a chart definition plus its
4
+ data compiled to a **geometry-free AST** and rendered as **pure-vnode
5
+ SVG** through [`@jarenjs/view`](../../packages/view) — no `innerHTML`,
6
+ no browser, no third-party chart library. Thirteen types: `pie` (with a
7
+ donut variant), `bar` (grouped/stacked, vertical/horizontal,
8
+ linear/log), `line` (linear/time/log, the streaming-critical type),
9
+ `scatter` (log axes, win/loss tones, reference line), `candlestick`
10
+ (OHLC on a time axis), `radar`, `gauge`, `boxplot` (raw samples or
11
+ five-number summaries, Tukey whiskers), `heatmap` (sequential blue
12
+ ramp, linear or log), `treemap` (squarified), `streamgraph`
13
+ (silhouette baseline), `sankey` (layered flows, cycle-safe) and `map`
14
+ (GeoJSON in Web Mercator, shaded by a feature property). Like
15
+ [`@jarenjs/mermaid`](../mermaid), it ships in **two layers**: a pure
16
+ engine (definition + data ⇄ AST ⇄ vnode) that knows only the vnode
17
+ shape, and a visual component that packages it for an `@jarenjs/app`
18
+ host.
19
+
20
+ ## A chart in one glance
21
+
22
+ ```js
23
+ import { compileChart } from '@jarenjs/charts';
24
+
25
+ const compiled = compileChart({
26
+ type: 'pie',
27
+ title: 'Pets',
28
+ slices: [
29
+ { label: 'Dogs', value: 40 },
30
+ { label: 'Cats', value: 25 },
31
+ { label: 'Birds', value: 10 },
32
+ ],
33
+ });
34
+
35
+ compiled.ast; // geometry-free: fractions and angles, no pixels
36
+ compiled.toVnode(); // ['svg', { viewBox, … }, …] — a pure-vnode SVG
37
+ compiled.toSvgString(); // standalone SVG text (SSR), colors baked in
38
+ ```
39
+
40
+ `config` carries the type and presentation fields; the data fields
41
+ (`slices`, …) default to the same object, so one self-contained
42
+ definition document works — while streaming callers pass live data as a
43
+ separate second argument: `compileChart(config, adapter.getData())`.
44
+
45
+ ## The component (for `@jarenjs/app` hosts)
46
+
47
+ ```js
48
+ import { createChartComponent } from '@jarenjs/charts/component';
49
+
50
+ const charts = createChartComponent({ theme: 'host' });
51
+ createApp(appDoc, {
52
+ viewModel: (state) => ({ ...state, chart: charts.view(state.config, state.data) }),
53
+ });
54
+ ```
55
+
56
+ `view()` memoizes on the **data object's identity** (a WeakMap): the
57
+ same `(config, data)` pair returns a reference-equal vnode, so an
58
+ unchanged chart patches in O(1); a fresh streaming snapshot re-renders.
59
+
60
+ ## The definition shapes
61
+
62
+ Every type reads its data from the definition document (or the
63
+ separate `data` argument). The type-specific fields, briefly:
64
+
65
+ | type | data fields |
66
+ |---|---|
67
+ | `pie` | `slices: [{label, value}]`; `donut: true` or a hole fraction in (0, 1) |
68
+ | `bar` | `categories`, `series: [{name, values}]`; `stacked`, `log`, `orient` |
69
+ | `line` | `series: [{name, points: [{x, y}]}]`; `x: 'time'`, `log`, `markers` |
70
+ | `scatter` | `points: [{x, y, tone?}]`; `xLog`/`yLog`, `refY`/`refLabel` |
71
+ | `candlestick` | `candles: [{t, open, high, low, close}]` |
72
+
73
+ **Time axes.** An `x: 'time'` axis (and every `candlestick`) accepts epoch milliseconds, a `Date`, or an **RFC 3339 string** — a date in a JSON document plots without being pre-converted, and a bare `2026-07-27` reads as UTC midnight. A *numeric* string still does not coerce: accepting `"5"` where the config asked for a number is a type confusion, not a date.
74
+
75
+ Ticks land on calendar boundaries rather than on the 1/2/5 ladder, because a quantity axis and a clock have different nice numbers — the ladder puts ticks 50 seconds or 8.64 days apart, which no reader converts back into a time. Steps come from a clock/calendar ladder (1/5/15/30 seconds and minutes, 1/3/6/12 hours, 1/2 days, 1/2 weeks, 1/3/6 months, years), and the label granularity follows the step, so an axis never repeats one string on every tick:
76
+
77
+ ```javascript
78
+ axisTicksTime(Date.UTC(2024, 0, 1), Date.UTC(2027, 0, 1)); // → 2024, 2025, 2026, 2027
79
+ axisTicksTime(Date.UTC(2026, 6, 27, 0), Date.UTC(2026, 6, 27, 6)); // → 00:00 … 06:00
80
+ ```
81
+ | `radar` | `axes: string[]`, `series: [{name, values}]`; `max` pins the domain, `labelEvery` the spoke-label stride |
82
+ | `gauge` | `value`; `min`/`max` (0..100 default), `unit`, `tone` |
83
+ | `boxplot` | `boxes: [{label, values}]` raw, or `{label, min, q1, med, q3, max, outliers?}` |
84
+ | `heatmap` | `xLabels`, `yLabels`, `values[yi][xi]` (row-major from the top); `log` |
85
+ | `treemap` | `items: [{label, value}]` or `[{label, children: [{label, value}]}]`; `aspect` (default 1.6) |
86
+ | `streamgraph` | `series: [{name, values}]`, optional numeric `xs` |
87
+ | `sankey` | `links: [{source, target, value}]` by name or index; `nodes` optional |
88
+
89
+ The full contract is `schemas/chart-definition.schema.json`.
90
+
91
+ ## Hover text and tooltips
92
+
93
+ Every value-carrying mark — slice, bar, dot, candle, cell, tile,
94
+ ribbon, band, series — renders a `<title>` child naming it and its
95
+ exact value. That is native hover text: it works in a static
96
+ `toSvgString()` document with no script, no CSS and no app, so it is
97
+ always on. Marks report the datum, not the axis's rounded tick scale,
98
+ which is the only way to read a value back off a log axis.
99
+
100
+ For a positioned, styled tooltip instead, ask for **bindings**:
101
+
102
+ ```js
103
+ const compiled = compileChart(config, data, {
104
+ tooltip: { action: 'chartHover', leaveAction: 'chartLeave' },
105
+ });
106
+ ```
107
+
108
+ Each mark then carries an `on` binding (VIEW-FORMAT §4) naming that
109
+ action, with the mark's descriptor as the payload and the pointer's
110
+ `clientX`/`clientY` requested as `$event` fields. Bindings are plain
111
+ JSON built at render time — the engine names an action, it never calls
112
+ one — and `renderToString` drops `on`, so the SSR bytes do not change.
113
+ The `@jarenjs/app` half is three lines and a projection:
114
+
115
+ ```js
116
+ const charts = createChartComponent({ theme: 'host', tooltip: { action: 'chartHover', leaveAction: 'chartLeave' } });
117
+ // action chartHover: { "tip": { "text": "$payload.text", "x": "$event.clientX", "y": "$event.clientY" } }
118
+ // action chartLeave: { "tip": null }
119
+ // viewModel: (state) => ({ ...state, tip: tooltipView(state.tip) })
120
+ ```
121
+
122
+ `tooltipView` (from `@jarenjs/charts/component`) returns the floating
123
+ box positioned at those viewport coordinates; `styles/charts.css`
124
+ carries its `.chart-tooltip` rules.
125
+
126
+ ## Scales, axes, palette
127
+
128
+ The core primitives are exported for reuse; scales are pure
129
+ `domain -> (value) => [0,1]` closures and axes generate nice-number
130
+ ticks (`1/2/5 × 10^k`) or log decades:
131
+
132
+ ```js
133
+ import {
134
+ scaleLinear, scaleLog, scaleBand, scaleTime,
135
+ axisTicksLinear, axisTicksLog, CATEGORICAL, SEQUENTIAL,
136
+ } from '@jarenjs/charts';
137
+ ```
138
+
139
+ The categorical palette is a concrete constant in the suite's anchor
140
+ order (blue first, amber second, teal third — no pink, no purple, per
141
+ the repo's docs/DESIGN.md), and `SEQUENTIAL` is its magnitude counterpart:
142
+ a single-hue blue ramp, light→dark, sampled continuously by
143
+ `sequentialColor(t)` (the heatmap's cell fill). The semantic tokens
144
+ (text/grid/axis, win/loss) resolve through the shared `resolveTheme`
145
+ kernel and can be host-linked with `theme: 'host'`, so charts follow
146
+ the site's light/dark flip live without a re-render.
147
+
148
+ ## Streaming (the adapter)
149
+
150
+ `@jarenjs/charts/stream-adapter` turns the unified reader events of
151
+ [`@jarenjs/josl`](../../packages/josl)'s streaming readers into chart
152
+ data — it consumes **events, never a reader**, so this package has no
153
+ parser dependency and any event source with the same `pair` shape
154
+ works:
155
+
156
+ ```js
157
+ import { createJsonxStreamReader } from '@jarenjs/josl/jsonx-stream';
158
+ import { createStreamAdapter } from '@jarenjs/charts/stream-adapter';
159
+ import { compileChart } from '@jarenjs/charts';
160
+
161
+ const adapter = createStreamAdapter('line', {
162
+ recordPath: ['run'], // records live at {"run": [...]} / [[run]]
163
+ xField: 'i', yField: 'ops', seriesField: 'suite',
164
+ maxPoints: 200, // ring-buffer eviction
165
+ });
166
+ const reader = createJsonxStreamReader({ mode: 'json', onEvent: adapter.onEvent });
167
+
168
+ for await (const chunk of feed) { // chunks may split ANY token
169
+ reader.feed(chunk);
170
+ render(compileChart({ type: 'line' }, adapter.getData()).toVnode());
171
+ }
172
+ reader.end();
173
+ adapter.endDocument();
174
+ ```
175
+
176
+ Two record boundaries cover the two streaming shapes: `'path'` (one
177
+ large document arriving in chunks — records close when the event path
178
+ leaves them) and `'document'` (many small complete documents, e.g. one
179
+ WebSocket message each — `endDocument()` closes the record;
180
+ `abortDocument()` discards a malformed one). Six accumulators:
181
+
182
+ | type | what a record contributes | fields |
183
+ |---|---|---|
184
+ | `line` | a point on a series, ring-buffer evicted | `xField`, `yField`, `seriesField` |
185
+ | `bar` | +1 (or `+yField`) on a category | `xField`, `yField?` |
186
+ | `heatmap` | the same, under two grouping keys | `xField` (column), `seriesField` (row), `yField?` |
187
+ | `gauge` | the latest reading; nothing is kept | `yField` |
188
+ | `candlestick` | a candle keyed by open time; a re-delivered key replaces it (exchange kline semantics) | `xField`, `openField`… |
189
+ | `map` | a whole GeoJSON Feature, projected and simplified on arrival | `labelField?`, `valueField?`, `simplify?`, `aspect?` |
190
+
191
+ A heatmap cell nobody measured stays `null` rather than `0` — the
192
+ surface shows through, which is the honest rendering of "no
193
+ measurement" — and a gauge with no reading yet is `null`, not zero.
194
+
195
+ The `map` accumulator is the odd one out: its record is not flat pair
196
+ fields but a complete Feature, consumed from the reader's `object-end`
197
+ events at `recordPath + [index]` (default `['features']`) — pair the
198
+ reader with `detach: ['features', '*']` so the document root retains
199
+ nothing and the accumulator's reduced set is the only retention. Each
200
+ feature's geometry is simplified **on arrival** to the vertices a
201
+ drawing of the current extent could distinguish, so memory is bounded
202
+ by the drawn detail rather than the source detail. The projection
203
+ cannot be fitted before the last feature has been seen, so the design
204
+ is refit-on-growth: the running bbox sets the tolerance, and when it
205
+ grows enough to double it, the kept features are coarsened once against
206
+ the new extent (never re-read — early features can only be finer than
207
+ needed, not wrong). `simplify: false` keeps every vertex, which unbounds
208
+ memory again; `test/charts/map-stream.test.js` pins the reduction, the
209
+ refit and the change-feed replay.
210
+
211
+ ## Incremental sessions (O(1) ticks)
212
+
213
+ A snapshot re-render is O(n): every point is re-projected because a
214
+ unit-space AST stores positions as *fractions of the domain*, so a
215
+ tick that moves the scales legitimately changes every mark. Declare a
216
+ **domain policy** and most ticks stop moving them — then
217
+ `createChartSession` patches only what changed:
218
+
219
+ ```js
220
+ const adapter = createStreamAdapter('line', { …, changes: true });
221
+ const session = createChartSession({
222
+ type: 'line',
223
+ domain: { y: 'step', x: { window: 60_000, slide: 15_000 } },
224
+ }, adapter);
225
+
226
+ reader.feed(chunk);
227
+ const { vnode, mode } = session.tick(); // 'incremental' | 'rebuilt' | 'unchanged'
228
+ ```
229
+
230
+ `{ changes: true }` makes the adapter buffer its mutations as RFC 6902
231
+ ops (`takeChanges()`); the session applies them to the previous AST and
232
+ rebuilds only the touched series — every other child stays
233
+ **reference-equal**, so the patcher skips it in O(1). Domain policies:
234
+
235
+ | policy | effect |
236
+ |---|---|
237
+ | `x: { window, slide }` | sliding window whose end is quantized to `slide` — the domain moves once per quantum, not per sample |
238
+ | `y: { min, max }` | pinned bounds; out-of-range samples clamp to the plot edge |
239
+ | `y: 'step'` | bounds snap outward to nice-number steps (decades under `log`) |
240
+
241
+ **The fallback is the design, not a failure mode.** When the domain
242
+ *does* move — or a new series appears, or the source resets — the
243
+ session rebuilds wholesale, because that is the correct rendering of a
244
+ frame whose scales moved. `mode` reports which path ran. The
245
+ correctness contract is byte equality: every tick's vnode serializes
246
+ identically to a wholesale `compileChart()` of the same data, which is
247
+ property-tested over thousands of random frames rather than assumed.
248
+
249
+ Measured (`npm run benchmark:charts`, one appended point):
250
+
251
+ | points × series | session tick | wholesale tick |
252
+ |---|---|---|
253
+ | 100 × 5 | ~8 µs | ~319 µs |
254
+ | 1 000 × 5 | ~4.6 µs | ~1.03 ms |
255
+ | 10 000 × 5 | ~4.2 µs | ~11.3 ms |
256
+
257
+ The session tick is *flat* in n (10 000 points cost no more than 100)
258
+ while the wholesale tick grows 35×. Supported types: `line` (appends
259
+ and ring-buffer evictions), `bar` (live counts and sums) and
260
+ `candlestick` (keyed kline upserts — one candle group re-renders). The
261
+ website's Binance demo runs on it.
262
+
263
+ A bar chart's stillness test is the nice-number top rather than a
264
+ declared policy: a count below it repaints one rect, a count that
265
+ pushes the axis higher rebuilds. A new category rebuilds too — every
266
+ band width and position moves with it — and so does a stacked chart,
267
+ where one value shifts every bar above it in its category.
268
+
269
+ | categories | session tick | wholesale tick |
270
+ |---|---|---|
271
+ | 20 | ~5.7 µs | ~33 µs |
272
+ | 200 | ~11 µs | ~137 µs |
273
+
274
+ Only the *vnode* work is O(1) there — one rect re-emitted instead of
275
+ all of them. The stillness test still rescans every category (a count
276
+ that drops can retire the tallest bar, so extremes cannot be extended)
277
+ and the adapter rebuilds its snapshot arrays, so the bar session's tick
278
+ does grow with the category count. It grows about 2× where the
279
+ wholesale render grows about 4×; the line session's flatness is the
280
+ stronger claim, and this is deliberately the weaker one.
281
+
282
+ ## Mermaid interop
283
+
284
+ `@jarenjs/mermaid` delegates its `pie` diagrams here (the arrow is
285
+ mermaid → charts, never the reverse); the
286
+ `@jarenjs/charts/transforms/mermaid-adapter` transform maps a mermaid
287
+ pie AST onto `compileChart` inputs.
288
+
289
+ ## Validation
290
+
291
+ `schemas/chart-definition.schema.json` describes the definition
292
+ document; validate untrusted definitions with `@jarenjs/validate`
293
+ before compiling.
@@ -0,0 +1,95 @@
1
+ /**
2
+ * @file The charts VISUAL COMPONENT — the only app-aware file in the
3
+ * package (the two-layer rule the mermaid component established). The
4
+ * engine neither knows nor needs any of it.
5
+ *
6
+ * `createChartComponent()` returns a memoized `view()` projection for
7
+ * `@jarenjs/app` viewModels: the same `(config, data)` pair yields a
8
+ * reference-equal vnode, so an unchanged chart patches in O(1). The
9
+ * memo is a WeakMap keyed on the DATA object's identity (streaming
10
+ * snapshots are fresh objects, so every tick re-renders; static data is
11
+ * stable, so navigation is free), with an inner cache keyed on the
12
+ * config's structural identity. That identity is the whole serialized
13
+ * config, not a fingerprint of it: the memo hands back the vnode, so a
14
+ * hash collision would draw one chart's config under another's.
15
+ */
16
+ export type ChartComponentOptions = {
17
+ /**
18
+ * theme name or override object
19
+ */
20
+ theme?: any;
21
+ /**
22
+ * pointer
23
+ * bindings emitted from every value mark, for a floating-tooltip host
24
+ * (see {@link tooltipView}); absent leaves charts on their native
25
+ * `<title>` hover only
26
+ */
27
+ tooltip?: import('../core/marks.js').ChartTooltipSpec;
28
+ };
29
+ export type ChartComponent = {
30
+ compile: (config: any, data?: any) => import('../core/chart.js').CompiledChart;
31
+ /**
32
+ * memoized vnode projection
33
+ */
34
+ view: (config: any, data?: any) => any;
35
+ /**
36
+ * incremental
37
+ * session bound to this component's theme
38
+ */
39
+ createSession: (config: any, source: import('../core/session.js').ChartSessionSource) => import('../core/session.js').ChartSession;
40
+ effects: Record<string, (props: any, dispatch: any) => any>;
41
+ };
42
+ /**
43
+ * @typedef {object} ChartComponentOptions
44
+ * @property {any} [theme] theme name or override object
45
+ * @property {import('../core/marks.js').ChartTooltipSpec} [tooltip] pointer
46
+ * bindings emitted from every value mark, for a floating-tooltip host
47
+ * (see {@link tooltipView}); absent leaves charts on their native
48
+ * `<title>` hover only
49
+ */
50
+ /**
51
+ * @typedef {object} ChartComponent
52
+ * @property {(config: any, data?: any) => import('../core/chart.js').CompiledChart} compile
53
+ * @property {(config: any, data?: any) => any} view memoized vnode projection
54
+ * @property {(config: any, source: import('../core/session.js').ChartSessionSource)
55
+ * => import('../core/session.js').ChartSession} createSession incremental
56
+ * session bound to this component's theme
57
+ * @property {Record<string, (props: any, dispatch: any) => any>} effects
58
+ */
59
+ /**
60
+ * Create the charts component.
61
+ *
62
+ * @example
63
+ * const charts = createChartComponent({ theme: 'host' });
64
+ * createApp(appDoc, {
65
+ * viewModel: (state) => ({ ...state, chart: charts.view(state.config, state.data) }),
66
+ * });
67
+ *
68
+ * @param {ChartComponentOptions} [options]
69
+ * @returns {ChartComponent}
70
+ */
71
+ export declare function createChartComponent(options?: ChartComponentOptions): ChartComponent;
72
+ /**
73
+ * The floating-tooltip host: the vnode for the box a `tooltip` binding
74
+ * asks for. The mark dispatches `{ text, … }` plus the pointer's
75
+ * `clientX`/`clientY`; an action stores that slice in the state and the
76
+ * viewModel projects it through here.
77
+ *
78
+ * Positioned `fixed` at the viewport coordinates the event carried, so
79
+ * it needs no measurement and no layout read — the stylesheet's
80
+ * `translate` lifts it clear of the pointer. `null` (the leave action's
81
+ * state) renders nothing.
82
+ *
83
+ * @example
84
+ * // viewModel: (state) => ({ ...state, tip: tooltipView(state.tip) })
85
+ * // action: { "tip": { "text": "$payload.text",
86
+ * // "x": "$event.clientX", "y": "$event.clientY" } }
87
+ *
88
+ * @param {{text?: string, x?: number, y?: number}|null|undefined} tip
89
+ * @returns {any} a `<div class="chart-tooltip">` vnode, or null
90
+ */
91
+ export declare function tooltipView(tip: {
92
+ text?: string;
93
+ x?: number;
94
+ y?: number;
95
+ } | null | undefined): any;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * @file Tick generators. Linear ticks snap to a nice-number step
3
+ * (1/2/5 × 10^k) so an axis never shows values like `12.75`; log ticks
4
+ * are decade powers (the shape benchmark-ratio charts need, where one
5
+ * axis spans two orders of magnitude); ordinal ticks center on their
6
+ * band. Tick *positions* are the caller's job via the matching scale —
7
+ * these functions return domain values only.
8
+ */
9
+ import { niceStep } from '@jarenjs/core/math';
10
+ export { niceStep };
11
+ /**
12
+ * Linear ticks: multiples of a nice step inside `[min, max]`.
13
+ * Degenerate domains yield a single tick.
14
+ * @param {number} min - Domain minimum
15
+ * @param {number} max - Domain maximum
16
+ * @param {number} [count] - Desired tick count (approximate)
17
+ * @returns {number[]}
18
+ */
19
+ export declare function axisTicksLinear(min: number, max: number, count?: number): number[];
20
+ /**
21
+ * Log ticks: the decade powers (… 0.1, 1, 10, 100 …) inside
22
+ * `[min, max]`. When the domain sits within a single decade the
23
+ * endpoints are returned so the axis is never empty.
24
+ * @param {number} min - Domain minimum (> 0)
25
+ * @param {number} max - Domain maximum (> 0)
26
+ * @returns {number[]}
27
+ */
28
+ export declare function axisTicksLog(min: number, max: number): number[];
29
+ /**
30
+ * Ordinal ticks: every category, centered on its band.
31
+ * @param {readonly string[]} categories - Domain, in display order
32
+ * @returns {{label: string, pos: number}[]} positions in [0,1]
33
+ */
34
+ export declare function axisTicksOrdinal(categories: readonly string[]): {
35
+ label: string;
36
+ pos: number;
37
+ }[];
38
+ /**
39
+ * A compact tick label: `1.5k`, `2M`, `1G` for large magnitudes,
40
+ * 3-significant-digit decimals otherwise.
41
+ * @param {number} v - Tick value
42
+ * @returns {string}
43
+ */
44
+ export declare function formatTickValue(v: number): string;
45
+ /**
46
+ * Choose the calendar step closest to covering `span` in `count` ticks.
47
+ * @param {number} span - Domain width in milliseconds
48
+ * @param {number} count - Desired tick count
49
+ * @returns {[string, number]} a [unit, amount] pair
50
+ */
51
+ export declare function niceTimeStep(span: number, count: number): [string, number];
52
+ /**
53
+ * Time ticks on calendar boundaries: the first tick is the start of a
54
+ * `[unit, amount]` step at or after `min`, and each following one is a
55
+ * whole step later. Month and year steps land on the first of the
56
+ * month, so a multi-year axis reads `2024 2025 2026` rather than
57
+ * arbitrary instants.
58
+ * @param {number} min - Domain minimum, epoch milliseconds
59
+ * @param {number} max - Domain maximum, epoch milliseconds
60
+ * @param {number} [count] - Desired tick count (approximate)
61
+ * @returns {number[]} tick values in epoch milliseconds
62
+ */
63
+ export declare function axisTicksTime(min: number, max: number, count?: number): number[];
64
+ /**
65
+ * A time-axis tick label in UTC (deterministic across machines).
66
+ *
67
+ * With a `step` the label matches the step's granularity — clock time
68
+ * for intraday ticks, a date for daily ones, a month or year above that
69
+ * — so an axis never repeats the same string on every tick. Without one
70
+ * it keeps the historical behaviour: `HH:MM:SS`, or the date when the
71
+ * value sits exactly on a day boundary.
72
+ *
73
+ * @param {number|Date} v - Epoch milliseconds or a Date
74
+ * @param {[string, number]} [step] - The [unit, amount] the axis stepped by
75
+ * @returns {string}
76
+ */
77
+ export declare function formatTimeTick(v: number | Date, step?: [string, number]): string;
@@ -0,0 +1,127 @@
1
+ /**
2
+ * @file The shared cartesian chrome: plot frame, axes, grid, legend and
3
+ * title, rendered from an AST's tick lists. Chart types append their
4
+ * marks inside the returned plot rect. All positions arrive in unit
5
+ * space (`pos`/`u`/`v` in [0,1], `v = 0` at the domain minimum, drawn
6
+ * at the plot bottom); pixel mapping happens only here and in the
7
+ * callers' mark loops.
8
+ */
9
+ export declare const FS_TICK = 11;
10
+ export declare const FS_LABEL = 12;
11
+ export declare const FS_TITLE = 15;
12
+ /**
13
+ * Resolve a mark color: semantic tone first, series palette otherwise.
14
+ * @param {{tokens: Record<string,string>}} theme
15
+ * @param {'win'|'loss'|null|undefined} tone
16
+ * @param {number} i series index
17
+ * @param {readonly string[]} [palette]
18
+ * @returns {string}
19
+ */
20
+ export declare function toneColor(theme: {
21
+ tokens: Record<string, string>;
22
+ }, tone: 'win' | 'loss' | null | undefined, i: number, palette?: readonly string[]): string;
23
+ /**
24
+ * Truncate a label so it fits `maxWidth` at `fontSize`, appending an
25
+ * ellipsis when anything was cut.
26
+ * @param {string} label
27
+ * @param {number} maxWidth
28
+ * @param {number} fontSize
29
+ * @returns {string}
30
+ */
31
+ export declare function fitLabel(label: string, maxWidth: number, fontSize: number): string;
32
+ /**
33
+ * The chart title `<text>`: bold, centred on `x`, carrying the
34
+ * `chart-title` class the chart stylesheet hooks. Every chart type places
35
+ * its own title — the anchor point and size are part of each type's
36
+ * layout — so those stay the caller's, and only the styling is shared.
37
+ * @param {number} x anchor (the title centres on it)
38
+ * @param {number} y baseline
39
+ * @param {string} text
40
+ * @param {number} fontSize
41
+ * @param {Record<string,string>} tokens the resolved theme tokens
42
+ * @returns {any}
43
+ */
44
+ export declare function chartTitle(x: number, y: number, text: string, fontSize: number, tokens: Record<string, string>): any;
45
+ export type LegendEntry = {
46
+ name: string;
47
+ swatch?: number;
48
+ tone?: 'win' | 'loss' | null;
49
+ };
50
+ /**
51
+ * @typedef {{name: string, swatch?: number, tone?: 'win'|'loss'|null}} LegendEntry
52
+ */
53
+ /**
54
+ * The legend swatch row, laid out right-to-left from `right` so the last
55
+ * entry ends at the plot's right edge. Returns the vnodes rather than
56
+ * drawing, so a chart with no cartesian frame at all (the map) can place
57
+ * the same row itself.
58
+ * @param {LegendEntry[]} legend
59
+ * @param {number} right the x the row ends at
60
+ * @param {number} y the swatches' top edge
61
+ * @param {{tokens: Record<string,string>}} theme
62
+ * @param {readonly string[]} [palette]
63
+ * @returns {any[]}
64
+ */
65
+ export declare function legendRow(legend: LegendEntry[], right: number, y: number, theme: {
66
+ tokens: Record<string, string>;
67
+ }, palette?: readonly string[]): any[];
68
+ export type AxisAST = {
69
+ ticks: {
70
+ pos: number;
71
+ label: string;
72
+ }[];
73
+ label?: string | null;
74
+ };
75
+ /**
76
+ * @typedef {{ticks: {pos: number, label: string}[], label?: string|null}} AxisAST
77
+ */
78
+ /**
79
+ * Compute the plot frame and render the chart chrome.
80
+ * @param {object} params
81
+ * @param {string|null} [params.title]
82
+ * @param {{name: string, swatch?: number, tone?: 'win'|'loss'|null}[]|null} [params.legend]
83
+ * @param {AxisAST} [params.xAxis] ticks along the bottom edge
84
+ * @param {AxisAST} [params.yAxis] ticks along the left edge
85
+ * @param {'x'|'y'|'xy'|'none'} [params.grid] which tick sets draw grid lines
86
+ * @param {number} [params.width]
87
+ * @param {number} [params.plotHeight]
88
+ * @param {readonly string[]} [params.palette]
89
+ * @param {{tokens: Record<string,string>}} params.theme
90
+ * @returns {{width: number, height: number, plot: {x:number,y:number,w:number,h:number}, children: any[]}}
91
+ */
92
+ export declare function cartesianFrame(params: {
93
+ title?: string | null;
94
+ legend?: {
95
+ name: string;
96
+ swatch?: number;
97
+ tone?: 'win' | 'loss' | null;
98
+ }[] | null;
99
+ xAxis?: AxisAST;
100
+ yAxis?: AxisAST;
101
+ grid?: 'x' | 'y' | 'xy' | 'none';
102
+ width?: number;
103
+ plotHeight?: number;
104
+ palette?: readonly string[];
105
+ theme: {
106
+ tokens: Record<string, string>;
107
+ };
108
+ }): {
109
+ width: number;
110
+ height: number;
111
+ plot: {
112
+ x: number;
113
+ y: number;
114
+ w: number;
115
+ h: number;
116
+ };
117
+ children: any[];
118
+ };
119
+ /**
120
+ * Stamp accessibility onto a finished chart svg vnode: an `aria-label`
121
+ * (the title) and a leading `<title>` child for hover text. `svgRoot`
122
+ * already sets `role="img"`.
123
+ * @param {any} svg the `['svg', props, …]` vnode (mutated in place)
124
+ * @param {string|null|undefined} label
125
+ * @returns {any} the same vnode
126
+ */
127
+ export declare function annotateChart(svg: any, label: string | null | undefined): any;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * @file `compileChart` — the type dispatcher. Compiles a chart
3
+ * definition plus its data into a bundle of cached projections
4
+ * (the `compileMermaid` shape): the geometry-free AST, a pure-vnode
5
+ * SVG, and a standalone SVG string.
6
+ *
7
+ * Memoization strategy (deliberate, see the component layer): identity
8
+ * of the *data object* is the cache key, held in a WeakMap by
9
+ * `createChartComponent` — never a hash of the object itself, which
10
+ * would stringify every object to `"[object Object]"` and collide
11
+ * universally. `contentKey` (the memo-grade stable-stringify hash from
12
+ * `@jarenjs/core/object`) derives the root vnode key from the config.
13
+ */
14
+ /**
15
+ * The chart types the dispatcher knows.
16
+ * @returns {string[]}
17
+ */
18
+ export declare function chartTypes(): string[];
19
+ export type CompiledChart = {
20
+ /**
21
+ * the geometry-free AST
22
+ */
23
+ ast: any;
24
+ /**
25
+ * cached pure-vnode SVG
26
+ */
27
+ toVnode: () => any;
28
+ /**
29
+ * cached standalone SVG string (SSR)
30
+ */
31
+ toSvgString: () => string;
32
+ };
33
+ /**
34
+ * @typedef {object} CompiledChart
35
+ * @property {any} ast the geometry-free AST
36
+ * @property {() => any} toVnode cached pure-vnode SVG
37
+ * @property {() => string} toSvgString cached standalone SVG string (SSR)
38
+ */
39
+ /**
40
+ * Compile a chart definition. `config` carries the type and
41
+ * presentation fields; `data` carries the series/slices and defaults to
42
+ * the config object itself, so a single self-contained definition
43
+ * document works while streaming callers pass live data separately.
44
+ *
45
+ * @param {{type: string, title?: string, [k: string]: any}} config
46
+ * @param {any} [data]
47
+ * @param {{theme?: any, tooltip?: import('./marks.js').ChartTooltipSpec}} [options]
48
+ * @returns {CompiledChart}
49
+ * @throws {TypeError} On an unknown chart type
50
+ */
51
+ export declare function compileChart(config: {
52
+ type: string;
53
+ title?: string;
54
+ [k: string]: any;
55
+ }, data?: any, options?: {
56
+ theme?: any;
57
+ tooltip?: import('./marks.js').ChartTooltipSpec;
58
+ }): CompiledChart;