@jarenjs/charts 0.56.0 → 0.67.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 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,6 +293,9 @@ 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
 
@@ -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}
@@ -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.56.0",
4
+ "version": "0.67.0",
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.56.0",
74
- "@jarenjs/view": "^0.56.0"
73
+ "@jarenjs/core": "^0.67.0",
74
+ "@jarenjs/view": "^0.67.0"
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
- // label patterns by how coarse the step is, compiled once
70
- const LABEL_SECOND = compileDateFormat('HH:mm:ss');
71
- const LABEL_MINUTE = compileDateFormat('HH:mm');
72
- const LABEL_DAY = compileDateFormat('yyyy-MM-dd');
73
- const LABEL_MONTH = compileDateFormat('yyyy-MM');
74
- const LABEL_YEAR = compileDateFormat('yyyy');
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
- const ms = typeof v === 'number' ? v : v.getTime();
91
- const parts = partsFromEpoch(ms);
92
- if (step === undefined) {
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) {
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, axisTicksTime, niceTimeStep,
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';
@@ -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: formatTimeTick(v, domains.xTickStep),
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, formatTimeTick,
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: time ? formatTimeTick(v, domains.xTickStep) : formatTickValue(v),
252
+ label: timeLabel !== null ? timeLabel(v, domains.xTickStep) : formatTickValue(v),
243
253
  })),
244
254
  label: config.xLabel ?? null,
245
255
  },