@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.
- package/README.md +293 -0
- package/dist/types/component/index.d.ts +95 -0
- package/dist/types/core/axis.d.ts +77 -0
- package/dist/types/core/cartesian.d.ts +127 -0
- package/dist/types/core/chart.d.ts +58 -0
- package/dist/types/core/domain.d.ts +96 -0
- package/dist/types/core/marks.d.ts +86 -0
- package/dist/types/core/palette.d.ts +72 -0
- package/dist/types/core/scale.d.ts +59 -0
- package/dist/types/core/session.d.ts +85 -0
- package/dist/types/core/stream-adapter.d.ts +187 -0
- package/dist/types/index.d.ts +35 -0
- package/dist/types/transforms/benchmark-adapter.d.ts +169 -0
- package/dist/types/transforms/mermaid-adapter.d.ts +30 -0
- package/dist/types/types/bar.d.ts +213 -0
- package/dist/types/types/boxplot.d.ts +116 -0
- package/dist/types/types/candlestick.d.ts +218 -0
- package/dist/types/types/gauge.d.ts +68 -0
- package/dist/types/types/heatmap.d.ts +104 -0
- package/dist/types/types/line.d.ts +272 -0
- package/dist/types/types/map.d.ts +137 -0
- package/dist/types/types/pie.d.ts +146 -0
- package/dist/types/types/radar.d.ts +89 -0
- package/dist/types/types/sankey.d.ts +100 -0
- package/dist/types/types/scatter.d.ts +80 -0
- package/dist/types/types/streamgraph.d.ts +75 -0
- package/dist/types/types/treemap.d.ts +118 -0
- package/package.json +76 -0
- package/schemas/chart-definition.schema.json +448 -0
- package/src/component/index.js +125 -0
- package/src/core/axis.js +221 -0
- package/src/core/cartesian.js +192 -0
- package/src/core/chart.js +101 -0
- package/src/core/domain.js +123 -0
- package/src/core/marks.js +110 -0
- package/src/core/palette.js +126 -0
- package/src/core/scale.js +106 -0
- package/src/core/session.js +0 -0
- package/src/core/stream-adapter.js +613 -0
- package/src/index.js +40 -0
- package/src/transforms/benchmark-adapter.js +298 -0
- package/src/transforms/mermaid-adapter.js +19 -0
- package/src/types/bar.js +276 -0
- package/src/types/boxplot.js +216 -0
- package/src/types/candlestick.js +274 -0
- package/src/types/gauge.js +140 -0
- package/src/types/heatmap.js +176 -0
- package/src/types/line.js +349 -0
- package/src/types/map.js +378 -0
- package/src/types/pie.js +163 -0
- package/src/types/radar.js +224 -0
- package/src/types/sankey.js +391 -0
- package/src/types/scatter.js +148 -0
- package/src/types/streamgraph.js +158 -0
- package/src/types/treemap.js +322 -0
- 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;
|