@jarenjs/charts 0.49.2 → 0.66.1
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 +72 -6
- package/dist/types/core/axis.d.ts +47 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/types/candlestick.d.ts +9 -1
- package/dist/types/types/line.d.ts +8 -1
- package/package.json +3 -3
- package/schemas/chart-definition.schema.json +26 -0
- package/src/core/axis.js +124 -21
- package/src/core/session.js +0 -0
- package/src/index.js +2 -1
- package/src/types/candlestick.js +14 -2
- package/src/types/line.js +13 -3
package/README.md
CHANGED
|
@@ -148,6 +148,51 @@ a single-hue blue ramp, light→dark, sampled continuously by
|
|
|
148
148
|
kernel and can be host-linked with `theme: 'host'`, so charts follow
|
|
149
149
|
the site's light/dark flip live without a re-render.
|
|
150
150
|
|
|
151
|
+
### A time axis in a language
|
|
152
|
+
|
|
153
|
+
A time axis labels its ticks by the granularity of the step they were
|
|
154
|
+
laid on — `HH:mm:ss`, `HH:mm`, `yyyy-MM-dd`, `yyyy-MM`, `yyyy` (the
|
|
155
|
+
`TIME_TICK_FORMATS` constant). Those are numeric, so nothing on a
|
|
156
|
+
default axis is mistranslated and nothing on it needs a language. A
|
|
157
|
+
`line` (with `x: 'time'`) or `candlestick` definition that wants month
|
|
158
|
+
or weekday names carries two flat members beside `x` and `xLabel`:
|
|
159
|
+
`timeFormats`, LDML patterns keyed by step granularity (`second`,
|
|
160
|
+
`minute`, `day`, `month`, `year`; an omitted member keeps its default),
|
|
161
|
+
and `dateNames`, the same `DateNames` record the Mermaid Gantt takes —
|
|
162
|
+
exactly what `@jarenjs/locales`' `compileDateLocale(pack).names`
|
|
163
|
+
produces:
|
|
164
|
+
|
|
165
|
+
```js
|
|
166
|
+
import { compileChart } from '@jarenjs/charts';
|
|
167
|
+
import { compileDateLocale, nl } from '@jarenjs/locales';
|
|
168
|
+
|
|
169
|
+
const chart = compileChart({
|
|
170
|
+
type: 'line', x: 'time',
|
|
171
|
+
dateNames: compileDateLocale(nl).names,
|
|
172
|
+
timeFormats: { day: 'EEEE d MMMM' },
|
|
173
|
+
series: [{ name: 'a', points: [
|
|
174
|
+
{ x: '2026-07-20', y: 1 }, { x: '2026-07-24', y: 3 }, { x: '2026-07-29', y: 2 },
|
|
175
|
+
] }],
|
|
176
|
+
});
|
|
177
|
+
chart.toSvgString(); // the ticks read 'maandag 20 juli', 'woensdag 22 juli', 'vrijdag 24 juli', …
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Both members are data: they serialise with the definition, validate
|
|
181
|
+
against the schema, and two charts on one page carry two records
|
|
182
|
+
without touching each other. The five patterns are compiled **once per
|
|
183
|
+
build** (`compileTimeTickFormat`, exported for a caller labelling its
|
|
184
|
+
own axis), never per label, so a two-hundred-tick axis costs five
|
|
185
|
+
compilations; a definition with neither member compiles nothing at all
|
|
186
|
+
and labels exactly as `formatTimeTick` does.
|
|
187
|
+
|
|
188
|
+
**Without a record a name token is a compile error, not a silent English
|
|
189
|
+
fallback.** This engine ships no month or weekday names of its own, so
|
|
190
|
+
`timeFormats: { day: 'EEEE d MMMM' }` with no `dateNames` throws a
|
|
191
|
+
`TypeError` naming the member and the way out — the same rule, in the
|
|
192
|
+
same words, as the Gantt's `axisFormat %B`. A numeric pattern needs no
|
|
193
|
+
record. No ambient locale is ever read: the record is supplied or
|
|
194
|
+
absent, never inferred.
|
|
195
|
+
|
|
151
196
|
## Streaming (the adapter)
|
|
152
197
|
|
|
153
198
|
`@jarenjs/charts/stream-adapter` turns the unified reader events of
|
|
@@ -248,20 +293,23 @@ frame whose scales moved. `mode` reports which path ran. The
|
|
|
248
293
|
correctness contract is byte equality: every tick's vnode serializes
|
|
249
294
|
identically to a wholesale `compileChart()` of the same data, which is
|
|
250
295
|
property-tested over thousands of random frames rather than assumed.
|
|
296
|
+
Line sources containing series without an array of `points` also rebuild
|
|
297
|
+
on changes: compilation filters those series, so source change indices
|
|
298
|
+
no longer match the rendered series indices.
|
|
251
299
|
|
|
252
300
|
Measured (`npm run benchmark:charts`, one appended point):
|
|
253
301
|
|
|
254
|
-
<!--
|
|
302
|
+
<!--fact:charts.sessionTable-->
|
|
255
303
|
| points × series | session tick | wholesale tick | frames incremental |
|
|
256
304
|
|---|---:|---:|---:|
|
|
257
305
|
| 100 × 5 | 6.26 µs | 301 µs | 1100 of 1100 |
|
|
258
306
|
| 1,000 × 5 | 4.84 µs | 1.1 ms | 1100 of 1100 |
|
|
259
307
|
| 10,000 × 5 | 4.44 µs | 8.61 ms | 1100 of 1100 |
|
|
260
308
|
| 10,000 × 5 *(sampled)* | 3.25 ms | 2.53 ms | 0 of 1100 |
|
|
261
|
-
<!--/
|
|
309
|
+
<!--/fact-->
|
|
262
310
|
|
|
263
311
|
The session tick is *flat* in n — it
|
|
264
|
-
moves <!--
|
|
312
|
+
moves <!--fact:charts.sessionFlatness-->0.7× while the wholesale tick grows 28.6×<!--/fact-->. Supported types: `line` (appends
|
|
265
313
|
and ring-buffer evictions), `bar` (live counts and sums) and
|
|
266
314
|
`candlestick` (keyed kline upserts — one candle group re-renders). The
|
|
267
315
|
website's Binance demo runs on it.
|
|
@@ -331,17 +379,17 @@ reports the data rather than the drawing.
|
|
|
331
379
|
|
|
332
380
|
Measured (`npm run benchmark:charts`, one series):
|
|
333
381
|
|
|
334
|
-
<!--
|
|
382
|
+
<!--fact:charts.samplingTable-->
|
|
335
383
|
| source points | drawn | method | source → AST | source → svg |
|
|
336
384
|
|---:|---:|---|---:|---:|
|
|
337
385
|
| 2,000 | 2,000 | none | 73.4 µs → 72.3 µs | 587 µs → 515 µs |
|
|
338
386
|
| 20,000 | 560 | lttb | 838 µs → 1.28 ms | 5.84 ms → 1.05 ms |
|
|
339
387
|
| 100,000 | 560 | lttb | 4.35 ms → 5.36 ms | 27.8 ms → 5.42 ms |
|
|
340
|
-
<!--/
|
|
388
|
+
<!--/fact-->
|
|
341
389
|
|
|
342
390
|
Choosing the points costs about what mapping them costs — the sampler
|
|
343
391
|
reads every reading either way — so the AST column is a small **loss**.
|
|
344
|
-
What it buys is the render — <!--
|
|
392
|
+
What it buys is the render — <!--fact:charts.samplingWin-->100,000 points draw as 560 and render 5.1× faster<!--/fact-->. The invariants above are asserted in the benchmark
|
|
345
393
|
before a single timing is printed.
|
|
346
394
|
|
|
347
395
|
**Sampling is not retention.** `createStreamAdapter`'s `maxPoints`
|
|
@@ -361,3 +409,21 @@ pie AST onto `compileChart` inputs.
|
|
|
361
409
|
`schemas/chart-definition.schema.json` describes the definition
|
|
362
410
|
document; validate untrusted definitions with `@jarenjs/validate`
|
|
363
411
|
before compiling.
|
|
412
|
+
|
|
413
|
+
## Exports
|
|
414
|
+
|
|
415
|
+
Every subpath a consumer can import, derived from the manifest by
|
|
416
|
+
`npm run docs:derive` (`npm run docs:check` fails when the two drift):
|
|
417
|
+
|
|
418
|
+
<!--fact:exports.charts-->
|
|
419
|
+
| Import | Kind | Declarations |
|
|
420
|
+
|---|---|---|
|
|
421
|
+
| `@jarenjs/charts` | JavaScript | declared |
|
|
422
|
+
| `@jarenjs/charts/component` | JavaScript | declared |
|
|
423
|
+
| `@jarenjs/charts/stream-adapter` | JavaScript | declared |
|
|
424
|
+
| `@jarenjs/charts/transforms/mermaid-adapter` | JavaScript | declared |
|
|
425
|
+
| `@jarenjs/charts/transforms/benchmark-adapter` | JavaScript | declared |
|
|
426
|
+
| `@jarenjs/charts/styles/charts.css` | asset | — |
|
|
427
|
+
| `@jarenjs/charts/schemas/chart-definition.schema.json` | schema | — |
|
|
428
|
+
| `@jarenjs/charts/package.json` | metadata | — |
|
|
429
|
+
<!--/fact-->
|
|
@@ -36,6 +36,50 @@ export declare function axisTicksOrdinal(categories: readonly string[]): {
|
|
|
36
36
|
* @returns {string}
|
|
37
37
|
*/
|
|
38
38
|
export declare function formatTickValue(v: number): string;
|
|
39
|
+
/**
|
|
40
|
+
* The label patterns a time axis compiles, keyed by the granularity of
|
|
41
|
+
* the step the ticks were laid on. Every default is numeric, so an axis
|
|
42
|
+
* with no `dateNames` record needs none; a caller overrides a member
|
|
43
|
+
* through `timeFormats` on the chart definition.
|
|
44
|
+
* @type {Readonly<Record<'second'|'minute'|'day'|'month'|'year', string>>}
|
|
45
|
+
*/
|
|
46
|
+
export declare const TIME_TICK_FORMATS: Readonly<Record<'second' | 'minute' | 'day' | 'month' | 'year', string>>;
|
|
47
|
+
export type TimeTickFormatOptions = {
|
|
48
|
+
/**
|
|
49
|
+
* - the
|
|
50
|
+
* month, weekday and meridiem names a name token (`MMMM`, `EEE`, `a`)
|
|
51
|
+
* reads; exactly the `names` member `compileDateLocale(pack)` returns
|
|
52
|
+
*/
|
|
53
|
+
dateNames?: import('@jarenjs/core/dates').DateNames;
|
|
54
|
+
/**
|
|
55
|
+
* - LDML patterns replacing the defaults in `TIME_TICK_FORMATS`, per
|
|
56
|
+
* step granularity
|
|
57
|
+
*/
|
|
58
|
+
timeFormats?: Partial<Record<'second' | 'minute' | 'day' | 'month' | 'year', string>>;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Build a time-axis labeller: the five step patterns compiled once,
|
|
62
|
+
* against the `dateNames` record the chart definition carries, so a
|
|
63
|
+
* render compiles per build and never per label. The result has the
|
|
64
|
+
* signature and rules of `formatTimeTick`. With neither member given it
|
|
65
|
+
* labels exactly as `formatTimeTick` does.
|
|
66
|
+
*
|
|
67
|
+
* A pattern with a name token (`MMMM`, `MMM`, `EEEE`, `EEE`, `a`) and no
|
|
68
|
+
* `dateNames` is a refusal — a `TypeError` — not a silent English
|
|
69
|
+
* fallback; the numeric defaults need no record.
|
|
70
|
+
*
|
|
71
|
+
* @param {TimeTickFormatOptions} [options]
|
|
72
|
+
* @returns {(v: number|Date, step?: [string, number]) => string}
|
|
73
|
+
* @throws {TypeError} on a name token with no `dateNames`, or a pattern
|
|
74
|
+
* that does not compile
|
|
75
|
+
* @example
|
|
76
|
+
* const label = compileTimeTickFormat({
|
|
77
|
+
* dateNames: compileDateLocale(nl).names,
|
|
78
|
+
* timeFormats: { day: 'EEEE d MMMM' },
|
|
79
|
+
* });
|
|
80
|
+
* label(Date.UTC(2026, 6, 27), ['day', 1]); // 'maandag 27 juli'
|
|
81
|
+
*/
|
|
82
|
+
export declare function compileTimeTickFormat(options?: TimeTickFormatOptions): (v: number | Date, step?: [string, number]) => string;
|
|
39
83
|
/**
|
|
40
84
|
* A time-axis tick label in UTC (deterministic across machines).
|
|
41
85
|
*
|
|
@@ -45,6 +89,9 @@ export declare function formatTickValue(v: number): string;
|
|
|
45
89
|
* it keeps the historical behaviour: `HH:MM:SS`, or the date when the
|
|
46
90
|
* value sits exactly on a day boundary.
|
|
47
91
|
*
|
|
92
|
+
* The patterns are the numeric defaults in `TIME_TICK_FORMATS`; a
|
|
93
|
+
* localized axis compiles its own labeller with `compileTimeTickFormat`.
|
|
94
|
+
*
|
|
48
95
|
* @param {number|Date} v - Epoch milliseconds or a Date
|
|
49
96
|
* @param {[string, number]} [step] - The [unit, amount] the axis stepped by
|
|
50
97
|
* @returns {string}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
export { compileChart, chartTypes } from './core/chart.js';
|
|
18
18
|
export { scaleLinear, scaleLog, scaleOrdinal, scaleBand, scaleTime } from './core/scale.js';
|
|
19
|
-
export { niceStep, axisTicksLinear, axisTicksLog, axisTicksOrdinal, formatTickValue, formatTimeTick, axisTicksTime, niceTimeStep, } from './core/axis.js';
|
|
19
|
+
export { niceStep, axisTicksLinear, axisTicksLog, axisTicksOrdinal, formatTickValue, formatTimeTick, compileTimeTickFormat, TIME_TICK_FORMATS, axisTicksTime, niceTimeStep, } from './core/axis.js';
|
|
20
20
|
export { CATEGORICAL, SEQUENTIAL, seriesColor, sequentialColor, inkFor, createTheme, THEMES, HOST_VARS } from './core/palette.js';
|
|
21
21
|
export { buildPieAST, renderPieAST } from './types/pie.js';
|
|
22
22
|
export { buildBarAST, renderBarAST } from './types/bar.js';
|
|
@@ -5,7 +5,15 @@
|
|
|
5
5
|
* host-linked (`--ok`/`--fail`) like every semantic color. Data shape:
|
|
6
6
|
*
|
|
7
7
|
* data = { candles: [{t, open, high, low, close}] }
|
|
8
|
-
* config = { type:'candlestick', title?, xLabel?, yLabel?, domain
|
|
8
|
+
* config = { type:'candlestick', title?, xLabel?, yLabel?, domain?,
|
|
9
|
+
* dateNames?, timeFormats? }
|
|
10
|
+
*
|
|
11
|
+
* The time axis labels its ticks through a labeller compiled once per
|
|
12
|
+
* build (`compileTimeTickFormat`) from the definition's optional
|
|
13
|
+
* `dateNames` record and `timeFormats` patterns; a definition without
|
|
14
|
+
* them labels exactly as `formatTimeTick` does. The per-candle hover
|
|
15
|
+
* text keeps the numeric `formatTimeTick` — it is a datum's timestamp,
|
|
16
|
+
* not an axis label.
|
|
9
17
|
*
|
|
10
18
|
* Candle width comes from the band-width math: an equal share of the
|
|
11
19
|
* axis per candle (klines arrive at a fixed interval, so equal bands
|
|
@@ -8,7 +8,14 @@
|
|
|
8
8
|
*
|
|
9
9
|
* data = { series: [{ name, points: [{x, y}] }] }
|
|
10
10
|
* config = { type:'line', title?, x?: 'linear'|'time', log?,
|
|
11
|
-
* markers?, xLabel?, yLabel?, domain?, sampling
|
|
11
|
+
* markers?, xLabel?, yLabel?, domain?, sampling?,
|
|
12
|
+
* dateNames?, timeFormats? }
|
|
13
|
+
*
|
|
14
|
+
* A time axis labels its ticks through a labeller compiled once per
|
|
15
|
+
* build (`compileTimeTickFormat`) from the definition's optional
|
|
16
|
+
* `dateNames` record and `timeFormats` patterns, so a localized axis
|
|
17
|
+
* costs five compilations per render and none per label, and a
|
|
18
|
+
* definition without them labels exactly as `formatTimeTick` does.
|
|
12
19
|
*
|
|
13
20
|
* `config.sampling` (`core/sampling.js`) decides how many of those
|
|
14
21
|
* points are drawn: above two thousand a time line is reduced through
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jarenjs/charts",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.66.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./dist/types/index.d.ts",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"prepack": "npm run build:types"
|
|
71
71
|
},
|
|
72
72
|
"dependencies": {
|
|
73
|
-
"@jarenjs/core": "^0.
|
|
74
|
-
"@jarenjs/view": "^0.
|
|
73
|
+
"@jarenjs/core": "^0.66.1",
|
|
74
|
+
"@jarenjs/view": "^0.66.1"
|
|
75
75
|
}
|
|
76
76
|
}
|
|
@@ -63,6 +63,8 @@
|
|
|
63
63
|
"yLabel": { "type": ["string", "null"] },
|
|
64
64
|
"domain": { "$ref": "#/$defs/domainPolicy" },
|
|
65
65
|
"sampling": { "$ref": "#/$defs/samplingPolicy" },
|
|
66
|
+
"dateNames": { "$ref": "#/$defs/dateNames" },
|
|
67
|
+
"timeFormats": { "$ref": "#/$defs/timeFormats" },
|
|
66
68
|
"series": {
|
|
67
69
|
"type": "array",
|
|
68
70
|
"items": { "$ref": "#/$defs/lineSeries" }
|
|
@@ -77,6 +79,8 @@
|
|
|
77
79
|
"xLabel": { "type": ["string", "null"] },
|
|
78
80
|
"yLabel": { "type": ["string", "null"] },
|
|
79
81
|
"domain": { "$ref": "#/$defs/domainPolicy" },
|
|
82
|
+
"dateNames": { "$ref": "#/$defs/dateNames" },
|
|
83
|
+
"timeFormats": { "$ref": "#/$defs/timeFormats" },
|
|
80
84
|
"candles": {
|
|
81
85
|
"type": "array",
|
|
82
86
|
"items": { "$ref": "#/$defs/candle" }
|
|
@@ -443,6 +447,28 @@
|
|
|
443
447
|
}
|
|
444
448
|
]
|
|
445
449
|
},
|
|
450
|
+
"dateNames": {
|
|
451
|
+
"description": "the month, weekday and meridiem names a time-axis pattern with a name token reads (compileDateLocale(pack).names from @jarenjs/locales); a name token with no record is a compile error, never an English fallback",
|
|
452
|
+
"type": "object",
|
|
453
|
+
"properties": {
|
|
454
|
+
"months": { "type": "array", "minItems": 12, "maxItems": 12, "items": { "type": "string" } },
|
|
455
|
+
"monthsShort": { "type": "array", "minItems": 12, "maxItems": 12, "items": { "type": "string" } },
|
|
456
|
+
"weekdays": { "type": "array", "minItems": 7, "maxItems": 7, "items": { "type": "string" } },
|
|
457
|
+
"weekdaysShort": { "type": "array", "minItems": 7, "maxItems": 7, "items": { "type": "string" } },
|
|
458
|
+
"meridiem": { "type": "array", "minItems": 2, "maxItems": 2, "items": { "type": "string" } }
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
"timeFormats": {
|
|
462
|
+
"description": "LDML label patterns for a time axis, keyed by the granularity of the tick step; an omitted member keeps its numeric default (HH:mm:ss, HH:mm, yyyy-MM-dd, yyyy-MM, yyyy)",
|
|
463
|
+
"type": "object",
|
|
464
|
+
"properties": {
|
|
465
|
+
"second": { "type": "string" },
|
|
466
|
+
"minute": { "type": "string" },
|
|
467
|
+
"day": { "type": "string" },
|
|
468
|
+
"month": { "type": "string" },
|
|
469
|
+
"year": { "type": "string" }
|
|
470
|
+
}
|
|
471
|
+
},
|
|
446
472
|
"domainPolicy": {
|
|
447
473
|
"description": "domain-stability policy: a quantized sliding x window and/or pinned or step-quantized y bounds, so most streaming ticks keep the scales still",
|
|
448
474
|
"type": "object",
|
package/src/core/axis.js
CHANGED
|
@@ -66,12 +66,124 @@ export function formatTickValue(v) {
|
|
|
66
66
|
return trim(v);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
69
|
+
/**
|
|
70
|
+
* The label patterns a time axis compiles, keyed by the granularity of
|
|
71
|
+
* the step the ticks were laid on. Every default is numeric, so an axis
|
|
72
|
+
* with no `dateNames` record needs none; a caller overrides a member
|
|
73
|
+
* through `timeFormats` on the chart definition.
|
|
74
|
+
* @type {Readonly<Record<'second'|'minute'|'day'|'month'|'year', string>>}
|
|
75
|
+
*/
|
|
76
|
+
export const TIME_TICK_FORMATS = Object.freeze({
|
|
77
|
+
second: 'HH:mm:ss',
|
|
78
|
+
minute: 'HH:mm',
|
|
79
|
+
day: 'yyyy-MM-dd',
|
|
80
|
+
month: 'yyyy-MM',
|
|
81
|
+
year: 'yyyy',
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @typedef {object} TimeTickFormatOptions
|
|
86
|
+
* @property {import('@jarenjs/core/dates').DateNames} [dateNames] - the
|
|
87
|
+
* month, weekday and meridiem names a name token (`MMMM`, `EEE`, `a`)
|
|
88
|
+
* reads; exactly the `names` member `compileDateLocale(pack)` returns
|
|
89
|
+
* @property {Partial<Record<'second'|'minute'|'day'|'month'|'year', string>>} [timeFormats]
|
|
90
|
+
* - LDML patterns replacing the defaults in `TIME_TICK_FORMATS`, per
|
|
91
|
+
* step granularity
|
|
92
|
+
*/
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Compile the five patterns once against a names record — the compile
|
|
96
|
+
* step of a two-stage labeller, so the per-label work is one walk of a
|
|
97
|
+
* compiled chain and never a pattern scan. A pattern that asks for a
|
|
98
|
+
* locale name with no record to answer it is refused here, at compile
|
|
99
|
+
* time, in the same terms the Mermaid Gantt refuses one: this engine
|
|
100
|
+
* ships no month or weekday names of its own.
|
|
101
|
+
* @param {TimeTickFormatOptions} options
|
|
102
|
+
* @returns {Record<'second'|'minute'|'day'|'month'|'year', (parts: object) => string>}
|
|
103
|
+
*/
|
|
104
|
+
function compileTickSet(options) {
|
|
105
|
+
const names = options.dateNames ?? undefined;
|
|
106
|
+
const patterns = options.timeFormats ?? undefined;
|
|
107
|
+
const set = /** @type {any} */ ({});
|
|
108
|
+
for (const key of /** @type {const} */ (['second', 'minute', 'day', 'month', 'year'])) {
|
|
109
|
+
const pattern = patterns?.[key] ?? TIME_TICK_FORMATS[key];
|
|
110
|
+
try {
|
|
111
|
+
set[key] = compileDateFormat(pattern, names);
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
115
|
+
if (names === undefined && message.includes('names provider')) {
|
|
116
|
+
throw new TypeError(`timeFormats.${key} '${pattern}' asks for a locale name, so it needs a`
|
|
117
|
+
+ " 'dateNames' record (a 'dateNames' member on the chart definition);"
|
|
118
|
+
+ ' this engine ships no month or weekday names of its own');
|
|
119
|
+
}
|
|
120
|
+
throw new TypeError(`timeFormats.${key} '${pattern}' cannot be compiled: ${message}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return set;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The label for one tick out of a compiled set — see `formatTimeTick`
|
|
128
|
+
* for the step and no-step rules.
|
|
129
|
+
* @param {Record<string, (parts: object) => string>} set
|
|
130
|
+
* @param {number|Date} v
|
|
131
|
+
* @param {[string, number]|undefined} step
|
|
132
|
+
* @returns {string}
|
|
133
|
+
*/
|
|
134
|
+
function labelTimeTick(set, v, step) {
|
|
135
|
+
const ms = typeof v === 'number' ? v : v.getTime();
|
|
136
|
+
const parts = partsFromEpoch(ms);
|
|
137
|
+
if (step === undefined) {
|
|
138
|
+
return parts.hours === 0 && parts.minutes === 0 && parts.seconds === 0
|
|
139
|
+
? set.day(parts)
|
|
140
|
+
: set.second(parts);
|
|
141
|
+
}
|
|
142
|
+
const unit = step[0];
|
|
143
|
+
if (unit === 'second')
|
|
144
|
+
return set.second(parts);
|
|
145
|
+
if (unit === 'minute' || unit === 'hour')
|
|
146
|
+
return set.minute(parts);
|
|
147
|
+
if (unit === 'day' || unit === 'week')
|
|
148
|
+
return set.day(parts);
|
|
149
|
+
return unit === 'month' ? set.month(parts) : set.year(parts);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Build a time-axis labeller: the five step patterns compiled once,
|
|
154
|
+
* against the `dateNames` record the chart definition carries, so a
|
|
155
|
+
* render compiles per build and never per label. The result has the
|
|
156
|
+
* signature and rules of `formatTimeTick`. With neither member given it
|
|
157
|
+
* labels exactly as `formatTimeTick` does.
|
|
158
|
+
*
|
|
159
|
+
* A pattern with a name token (`MMMM`, `MMM`, `EEEE`, `EEE`, `a`) and no
|
|
160
|
+
* `dateNames` is a refusal — a `TypeError` — not a silent English
|
|
161
|
+
* fallback; the numeric defaults need no record.
|
|
162
|
+
*
|
|
163
|
+
* @param {TimeTickFormatOptions} [options]
|
|
164
|
+
* @returns {(v: number|Date, step?: [string, number]) => string}
|
|
165
|
+
* @throws {TypeError} on a name token with no `dateNames`, or a pattern
|
|
166
|
+
* that does not compile
|
|
167
|
+
* @example
|
|
168
|
+
* const label = compileTimeTickFormat({
|
|
169
|
+
* dateNames: compileDateLocale(nl).names,
|
|
170
|
+
* timeFormats: { day: 'EEEE d MMMM' },
|
|
171
|
+
* });
|
|
172
|
+
* label(Date.UTC(2026, 6, 27), ['day', 1]); // 'maandag 27 juli'
|
|
173
|
+
*/
|
|
174
|
+
export function compileTimeTickFormat(options = {}) {
|
|
175
|
+
// nothing to compile against: the shared record-free labeller, so a
|
|
176
|
+
// definition with neither member costs no compilation per build
|
|
177
|
+
if (options.dateNames === undefined && options.timeFormats === undefined)
|
|
178
|
+
return formatTimeTick;
|
|
179
|
+
const set = compileTickSet(options);
|
|
180
|
+
return (v, step = undefined) => labelTimeTick(set, v, step);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// The record-free set behind `formatTimeTick`: compiled on first use and
|
|
184
|
+
// kept, so a render with no `dateNames` compiles nothing at all.
|
|
185
|
+
/** @type {Record<string, (parts: object) => string>|null} */
|
|
186
|
+
let DEFAULT_SET = null;
|
|
75
187
|
|
|
76
188
|
/**
|
|
77
189
|
* A time-axis tick label in UTC (deterministic across machines).
|
|
@@ -82,26 +194,17 @@ const LABEL_YEAR = compileDateFormat('yyyy');
|
|
|
82
194
|
* it keeps the historical behaviour: `HH:MM:SS`, or the date when the
|
|
83
195
|
* value sits exactly on a day boundary.
|
|
84
196
|
*
|
|
197
|
+
* The patterns are the numeric defaults in `TIME_TICK_FORMATS`; a
|
|
198
|
+
* localized axis compiles its own labeller with `compileTimeTickFormat`.
|
|
199
|
+
*
|
|
85
200
|
* @param {number|Date} v - Epoch milliseconds or a Date
|
|
86
201
|
* @param {[string, number]} [step] - The [unit, amount] the axis stepped by
|
|
87
202
|
* @returns {string}
|
|
88
203
|
*/
|
|
89
204
|
export function formatTimeTick(v, step = undefined) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
return parts.hours === 0 && parts.minutes === 0 && parts.seconds === 0
|
|
94
|
-
? LABEL_DAY(parts)
|
|
95
|
-
: LABEL_SECOND(parts);
|
|
96
|
-
}
|
|
97
|
-
const unit = step[0];
|
|
98
|
-
if (unit === 'second')
|
|
99
|
-
return LABEL_SECOND(parts);
|
|
100
|
-
if (unit === 'minute' || unit === 'hour')
|
|
101
|
-
return LABEL_MINUTE(parts);
|
|
102
|
-
if (unit === 'day' || unit === 'week')
|
|
103
|
-
return LABEL_DAY(parts);
|
|
104
|
-
return unit === 'month' ? LABEL_MONTH(parts) : LABEL_YEAR(parts);
|
|
205
|
+
if (DEFAULT_SET === null)
|
|
206
|
+
DEFAULT_SET = compileTickSet({});
|
|
207
|
+
return labelTimeTick(DEFAULT_SET, v, step);
|
|
105
208
|
}
|
|
106
209
|
|
|
107
210
|
function trim(x) {
|
package/src/core/session.js
CHANGED
|
Binary file
|
package/src/index.js
CHANGED
|
@@ -20,7 +20,8 @@ export { compileChart, chartTypes } from './core/chart.js';
|
|
|
20
20
|
export { scaleLinear, scaleLog, scaleOrdinal, scaleBand, scaleTime } from './core/scale.js';
|
|
21
21
|
export {
|
|
22
22
|
niceStep, axisTicksLinear, axisTicksLog, axisTicksOrdinal,
|
|
23
|
-
formatTickValue, formatTimeTick,
|
|
23
|
+
formatTickValue, formatTimeTick, compileTimeTickFormat, TIME_TICK_FORMATS,
|
|
24
|
+
axisTicksTime, niceTimeStep,
|
|
24
25
|
} from './core/axis.js';
|
|
25
26
|
export { CATEGORICAL, SEQUENTIAL, seriesColor, sequentialColor, inkFor, createTheme, THEMES, HOST_VARS } from './core/palette.js';
|
|
26
27
|
export { buildPieAST, renderPieAST } from './types/pie.js';
|
package/src/types/candlestick.js
CHANGED
|
@@ -6,7 +6,15 @@
|
|
|
6
6
|
* host-linked (`--ok`/`--fail`) like every semantic color. Data shape:
|
|
7
7
|
*
|
|
8
8
|
* data = { candles: [{t, open, high, low, close}] }
|
|
9
|
-
* config = { type:'candlestick', title?, xLabel?, yLabel?, domain
|
|
9
|
+
* config = { type:'candlestick', title?, xLabel?, yLabel?, domain?,
|
|
10
|
+
* dateNames?, timeFormats? }
|
|
11
|
+
*
|
|
12
|
+
* The time axis labels its ticks through a labeller compiled once per
|
|
13
|
+
* build (`compileTimeTickFormat`) from the definition's optional
|
|
14
|
+
* `dateNames` record and `timeFormats` patterns; a definition without
|
|
15
|
+
* them labels exactly as `formatTimeTick` does. The per-candle hover
|
|
16
|
+
* text keeps the numeric `formatTimeTick` — it is a datum's timestamp,
|
|
17
|
+
* not an axis label.
|
|
10
18
|
*
|
|
11
19
|
* Candle width comes from the band-width math: an equal share of the
|
|
12
20
|
* axis per candle (klines arrive at a fixed interval, so equal bands
|
|
@@ -32,6 +40,7 @@ import { clamp01 } from '@jarenjs/core/math';
|
|
|
32
40
|
import { scaleLinear, scaleTime } from '../core/scale.js';
|
|
33
41
|
import {
|
|
34
42
|
axisTicksLinear, axisTicksTime, niceTimeStep, formatTickValue, formatTimeTick,
|
|
43
|
+
compileTimeTickFormat,
|
|
35
44
|
} from '../core/axis.js';
|
|
36
45
|
import { cartesianFrame, annotateChart } from '../core/cartesian.js';
|
|
37
46
|
import { normalizeTooltip, markProps } from '../core/marks.js';
|
|
@@ -173,6 +182,9 @@ export function buildCandlestickAST(data, config = {}) {
|
|
|
173
182
|
const domains = resolveCandleDomains(ext, policy);
|
|
174
183
|
const xScale = scaleTime(domains.x[0], domains.x[1]);
|
|
175
184
|
const yScale = scaleLinear(domains.y[0], domains.y[1]);
|
|
185
|
+
const timeLabel = compileTimeTickFormat({
|
|
186
|
+
dateNames: config.dateNames, timeFormats: config.timeFormats,
|
|
187
|
+
});
|
|
176
188
|
|
|
177
189
|
const w = ext.kept.length === 0 ? 0.1 : (1 / ext.kept.length) * 0.7;
|
|
178
190
|
const candles = ext.kept.map((c) => candleUnit(c, xScale, yScale, w));
|
|
@@ -182,7 +194,7 @@ export function buildCandlestickAST(data, config = {}) {
|
|
|
182
194
|
title: config.title ?? null,
|
|
183
195
|
x: {
|
|
184
196
|
ticks: domains.xTickValues.map((v) => ({
|
|
185
|
-
pos: clamp01(xScale(v)), label:
|
|
197
|
+
pos: clamp01(xScale(v)), label: timeLabel(v, domains.xTickStep),
|
|
186
198
|
})),
|
|
187
199
|
label: config.xLabel ?? null,
|
|
188
200
|
},
|
package/src/types/line.js
CHANGED
|
@@ -9,7 +9,14 @@
|
|
|
9
9
|
*
|
|
10
10
|
* data = { series: [{ name, points: [{x, y}] }] }
|
|
11
11
|
* config = { type:'line', title?, x?: 'linear'|'time', log?,
|
|
12
|
-
* markers?, xLabel?, yLabel?, domain?, sampling
|
|
12
|
+
* markers?, xLabel?, yLabel?, domain?, sampling?,
|
|
13
|
+
* dateNames?, timeFormats? }
|
|
14
|
+
*
|
|
15
|
+
* A time axis labels its ticks through a labeller compiled once per
|
|
16
|
+
* build (`compileTimeTickFormat`) from the definition's optional
|
|
17
|
+
* `dateNames` record and `timeFormats` patterns, so a localized axis
|
|
18
|
+
* costs five compilations per render and none per label, and a
|
|
19
|
+
* definition without them labels exactly as `formatTimeTick` does.
|
|
13
20
|
*
|
|
14
21
|
* `config.sampling` (`core/sampling.js`) decides how many of those
|
|
15
22
|
* points are drawn: above two thousand a time line is reduced through
|
|
@@ -39,7 +46,7 @@ import { clamp01 } from '@jarenjs/core/math';
|
|
|
39
46
|
import { scaleLinear, scaleLog, scaleTime } from '../core/scale.js';
|
|
40
47
|
import {
|
|
41
48
|
axisTicksLinear, axisTicksLog, axisTicksTime, niceTimeStep,
|
|
42
|
-
formatTickValue,
|
|
49
|
+
formatTickValue, compileTimeTickFormat,
|
|
43
50
|
} from '../core/axis.js';
|
|
44
51
|
import { cartesianFrame, annotateChart } from '../core/cartesian.js';
|
|
45
52
|
import { numOf } from '../core/stream-adapter.js';
|
|
@@ -213,6 +220,9 @@ export function buildLineAST(data, config = {}) {
|
|
|
213
220
|
const policy = normalizeDomainPolicy(config.domain);
|
|
214
221
|
const domains = resolveLineDomains(scanLineExtremes(input, policy, log), policy, time, log);
|
|
215
222
|
const { xScale, yScale } = lineScales(domains, time, log);
|
|
223
|
+
const timeLabel = time
|
|
224
|
+
? compileTimeTickFormat({ dateNames: config.dateNames, timeFormats: config.timeFormats })
|
|
225
|
+
: null;
|
|
216
226
|
|
|
217
227
|
// Sampling chooses which points are DRAWN; it never moves a domain,
|
|
218
228
|
// which is scanned from every source point above. A series the policy
|
|
@@ -239,7 +249,7 @@ export function buildLineAST(data, config = {}) {
|
|
|
239
249
|
x: {
|
|
240
250
|
ticks: domains.xTickValues.map((v) => ({
|
|
241
251
|
pos: clamp01(xScale(v)),
|
|
242
|
-
label:
|
|
252
|
+
label: timeLabel !== null ? timeLabel(v, domains.xTickStep) : formatTickValue(v),
|
|
243
253
|
})),
|
|
244
254
|
label: config.xLabel ?? null,
|
|
245
255
|
},
|