@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
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Domain-stability policies for the streaming chart types. A
|
|
4
|
+
* unit-space AST stores every position as a fraction of its domain, so
|
|
5
|
+
* a tick that moves the domain legitimately changes every mark — a
|
|
6
|
+
* wholesale re-render is then correct. These policies exist so that
|
|
7
|
+
* most streaming ticks do NOT move the domain, which is what makes
|
|
8
|
+
* incremental re-render (the chart session) possible:
|
|
9
|
+
*
|
|
10
|
+
* - `x: { window, slide? }` — sliding x window of fixed span `window`,
|
|
11
|
+
* whose END is quantized to multiples of `slide` (default a quarter
|
|
12
|
+
* window). The domain therefore moves once per `slide` of x
|
|
13
|
+
* progress, not once per sample; samples older than the window are
|
|
14
|
+
* dropped from the plot.
|
|
15
|
+
* - `y: { min?, max? }` — pinned value bounds; samples beyond them
|
|
16
|
+
* clamp to the plot edge (the render's clamp01 already does this).
|
|
17
|
+
* - `y: 'step'` — hysteresis by quantization: the value domain snaps
|
|
18
|
+
* outward to nice-number multiples (decades under a log axis), so it
|
|
19
|
+
* changes only when a sample crosses a step boundary.
|
|
20
|
+
*
|
|
21
|
+
* All resolution is pure — policies are declared in the config and
|
|
22
|
+
* resolved from the data extremes on every build; stability comes from
|
|
23
|
+
* quantization, not hidden state.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { niceStep } from './axis.js';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {object} DomainPolicy
|
|
30
|
+
* @property {number|null} window sliding x span (null = none)
|
|
31
|
+
* @property {number|null} slide window end quantum (null = window/4)
|
|
32
|
+
* @property {{min: number|null, max: number|null}|null} pin pinned y bounds
|
|
33
|
+
* @property {boolean} step quantized y domain
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Normalize a config `domain` member into a policy object; hostile or
|
|
38
|
+
* absent input yields the all-null policy (today's behavior).
|
|
39
|
+
* @param {any} domain the config `domain` member
|
|
40
|
+
* @returns {DomainPolicy}
|
|
41
|
+
*/
|
|
42
|
+
export function normalizeDomainPolicy(domain) {
|
|
43
|
+
const none = { window: null, slide: null, pin: null, step: false };
|
|
44
|
+
if (domain === null || typeof domain !== 'object') return none;
|
|
45
|
+
const policy = { ...none };
|
|
46
|
+
const x = domain.x;
|
|
47
|
+
if (x !== null && typeof x === 'object'
|
|
48
|
+
&& typeof x.window === 'number' && Number.isFinite(x.window) && x.window > 0) {
|
|
49
|
+
policy.window = x.window;
|
|
50
|
+
if (typeof x.slide === 'number' && Number.isFinite(x.slide) && x.slide > 0)
|
|
51
|
+
policy.slide = x.slide;
|
|
52
|
+
}
|
|
53
|
+
const y = domain.y;
|
|
54
|
+
if (y === 'step') {
|
|
55
|
+
policy.step = true;
|
|
56
|
+
}
|
|
57
|
+
else if (y !== null && typeof y === 'object') {
|
|
58
|
+
const min = typeof y.min === 'number' && Number.isFinite(y.min) ? y.min : null;
|
|
59
|
+
const max = typeof y.max === 'number' && Number.isFinite(y.max) ? y.max : null;
|
|
60
|
+
if (min !== null || max !== null) policy.pin = { min, max };
|
|
61
|
+
}
|
|
62
|
+
return policy;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Resolve the windowed x domain `[end - window, end]`: the end is the
|
|
67
|
+
* smallest multiple of the slide quantum at or above the newest sample,
|
|
68
|
+
* so it moves once per quantum, not once per sample.
|
|
69
|
+
* @param {number} xMax newest x in the data (non-finite = empty data)
|
|
70
|
+
* @param {number} window the window span (> 0)
|
|
71
|
+
* @param {number|null} slide the end quantum (null = window / 4)
|
|
72
|
+
* @returns {[number, number]}
|
|
73
|
+
*/
|
|
74
|
+
export function resolveWindowX(xMax, window, slide) {
|
|
75
|
+
const quantum = slide ?? window / 4;
|
|
76
|
+
const end = Number.isFinite(xMax) ? quantum * Math.ceil(xMax / quantum) : window;
|
|
77
|
+
return [end - window, end];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Resolve the quantized (`'step'`) y domain: extremes snapped outward
|
|
82
|
+
* to multiples of a nice step of the span, so small new extremes
|
|
83
|
+
* usually land inside the current domain.
|
|
84
|
+
* @param {number} yMin @param {number} yMax data extremes (finite)
|
|
85
|
+
* @returns {[number, number]}
|
|
86
|
+
*/
|
|
87
|
+
export function resolveStepY(yMin, yMax) {
|
|
88
|
+
const span = yMax - yMin;
|
|
89
|
+
const step = niceStep(span > 0 ? span : Math.abs(yMax) || 1, 4);
|
|
90
|
+
let lo = step * Math.floor(yMin / step);
|
|
91
|
+
let hi = step * Math.ceil(yMax / step);
|
|
92
|
+
if (hi === lo) hi = lo + step;
|
|
93
|
+
return [lo, hi];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Resolve the quantized y domain under a log axis: decade bounds
|
|
98
|
+
* (`10^floor` / `10^ceil`), the log counterpart of {@link resolveStepY}.
|
|
99
|
+
* @param {number} yMin @param {number} yMax data extremes (> 0)
|
|
100
|
+
* @returns {[number, number]}
|
|
101
|
+
*/
|
|
102
|
+
export function resolveStepYLog(yMin, yMax) {
|
|
103
|
+
const lo = Math.pow(10, Math.floor(Math.log10(yMin)));
|
|
104
|
+
let hi = Math.pow(10, Math.ceil(Math.log10(yMax)));
|
|
105
|
+
if (hi === lo) hi = lo * 10;
|
|
106
|
+
return [lo, hi];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Resolve pinned y bounds over the data extremes. Under a log axis a
|
|
111
|
+
* non-positive pin is ignored (a hostile pin never breaks the scale);
|
|
112
|
+
* a pin pair that closes the domain falls back to the data extremes.
|
|
113
|
+
* @param {number} yMin @param {number} yMax data extremes
|
|
114
|
+
* @param {{min: number|null, max: number|null}} pin
|
|
115
|
+
* @param {boolean} log
|
|
116
|
+
* @returns {[number, number]}
|
|
117
|
+
*/
|
|
118
|
+
export function resolvePinnedY(yMin, yMax, pin, log) {
|
|
119
|
+
let lo = pin.min !== null && (!log || pin.min > 0) ? pin.min : yMin;
|
|
120
|
+
let hi = pin.max !== null && (!log || pin.max > 0) ? pin.max : yMax;
|
|
121
|
+
if (hi <= lo) { lo = yMin; hi = yMax; }
|
|
122
|
+
return [lo, hi];
|
|
123
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Value marks — the two things every value-carrying mark carries.
|
|
4
|
+
*
|
|
5
|
+
* 1. A `<title>` child holding the mark's hover text. Native SSR-safe
|
|
6
|
+
* hover: it works in a static `toSvgString()` document with no
|
|
7
|
+
* script, no CSS and no app, which is why it is unconditional.
|
|
8
|
+
* 2. Opt-in pointer **bindings** (`on`, VIEW-FORMAT §4) for a host that
|
|
9
|
+
* wants a positioned floating tooltip instead. Bindings are plain
|
|
10
|
+
* JSON built at render time, so the engine stays pure — it names an
|
|
11
|
+
* action, it never calls one — and `renderToString` drops `on`
|
|
12
|
+
* entirely, so the SSR bytes are the same either way.
|
|
13
|
+
*
|
|
14
|
+
* The binding's `with` payload is the mark descriptor: its `text` (the
|
|
15
|
+
* same string the `<title>` carries, so a host needs nothing else to
|
|
16
|
+
* draw a box) plus whatever identifies the mark for a richer host.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {object} ChartTooltipSpec
|
|
21
|
+
* @property {string} action action dispatched when the pointer enters a mark
|
|
22
|
+
* @property {string} [leaveAction] action dispatched when it leaves
|
|
23
|
+
* @property {string} [enter] enter event name (default `'pointerenter'`)
|
|
24
|
+
* @property {string} [leave] leave event name (default `'pointerleave'`)
|
|
25
|
+
* @property {string[]} [event] `$event` fields to request (default
|
|
26
|
+
* `['clientX', 'clientY']` — what a floating box needs to position itself)
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {object} ChartTooltip resolved spec
|
|
30
|
+
* @property {string} action
|
|
31
|
+
* @property {string|null} leaveAction
|
|
32
|
+
* @property {string} enter @property {string} leave
|
|
33
|
+
* @property {string[]} event
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/** The pointer coordinates a floating tooltip positions itself from. */
|
|
37
|
+
const POINTER_FIELDS = ['clientX', 'clientY'];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve a tooltip spec. Anything that does not name an action
|
|
41
|
+
* resolves to `null` — bindings off, `<title>` hover only — so a
|
|
42
|
+
* hostile or half-written spec degrades to the static rendering rather
|
|
43
|
+
* than emitting a binding no action answers.
|
|
44
|
+
* @param {ChartTooltipSpec|null|undefined|any} spec
|
|
45
|
+
* @returns {ChartTooltip|null}
|
|
46
|
+
*/
|
|
47
|
+
export function normalizeTooltip(spec) {
|
|
48
|
+
if (spec === null || typeof spec !== 'object') return null;
|
|
49
|
+
if (typeof spec.action !== 'string' || spec.action === '') return null;
|
|
50
|
+
const name = (v, fallback) => typeof v === 'string' && v !== '' ? v : fallback;
|
|
51
|
+
return {
|
|
52
|
+
action: spec.action,
|
|
53
|
+
leaveAction: name(spec.leaveAction, null),
|
|
54
|
+
enter: name(spec.enter, 'pointerenter'),
|
|
55
|
+
leave: name(spec.leave, 'pointerleave'),
|
|
56
|
+
event: Array.isArray(spec.event)
|
|
57
|
+
? spec.event.filter((f) => typeof f === 'string')
|
|
58
|
+
: POINTER_FIELDS,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The `on` binding object for one mark, or `undefined` when tooltips
|
|
64
|
+
* are off (so callers can keep passing their props object through
|
|
65
|
+
* untouched — an unbound chart allocates nothing extra).
|
|
66
|
+
* @param {ChartTooltip|null} tooltip
|
|
67
|
+
* @param {string} text the mark's hover text
|
|
68
|
+
* @param {Record<string, any>} [descriptor] extra `with` members
|
|
69
|
+
* @returns {Record<string, any>|undefined}
|
|
70
|
+
*/
|
|
71
|
+
export function markBinding(tooltip, text, descriptor) {
|
|
72
|
+
if (tooltip === null) return undefined;
|
|
73
|
+
const on = {
|
|
74
|
+
[tooltip.enter]: {
|
|
75
|
+
action: tooltip.action,
|
|
76
|
+
with: descriptor === undefined ? { text } : { text, ...descriptor },
|
|
77
|
+
event: tooltip.event,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
if (tooltip.leaveAction !== null) on[tooltip.leave] = { action: tooltip.leaveAction };
|
|
81
|
+
return on;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Mark props with the tooltip bindings folded in — for a mark whose
|
|
86
|
+
* `<title>` sits among other children (a series or candle `<g>`).
|
|
87
|
+
* @param {Record<string, any>} props
|
|
88
|
+
* @param {ChartTooltip|null} tooltip
|
|
89
|
+
* @param {string} text
|
|
90
|
+
* @param {Record<string, any>} [descriptor]
|
|
91
|
+
* @returns {Record<string, any>}
|
|
92
|
+
*/
|
|
93
|
+
export function markProps(props, tooltip, text, descriptor) {
|
|
94
|
+
const on = markBinding(tooltip, text, descriptor);
|
|
95
|
+
return on === undefined ? props : { ...props, on };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* A leaf value mark: its element, its hover `<title>`, and the tooltip
|
|
100
|
+
* bindings when a host asked for them.
|
|
101
|
+
* @param {string} tag
|
|
102
|
+
* @param {Record<string, any>} props
|
|
103
|
+
* @param {ChartTooltip|null} tooltip
|
|
104
|
+
* @param {string} text hover text
|
|
105
|
+
* @param {Record<string, any>} [descriptor] extra `with` members
|
|
106
|
+
* @returns {any} the mark vnode
|
|
107
|
+
*/
|
|
108
|
+
export function valueMark(tag, props, tooltip, text, descriptor) {
|
|
109
|
+
return [tag, markProps(props, tooltip, text, descriptor), ['title', {}, text]];
|
|
110
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Colors: the categorical palette and the theme token tables.
|
|
4
|
+
*
|
|
5
|
+
* The categorical palette is a concrete constant, not a set of theme
|
|
6
|
+
* tokens (docs/DESIGN.md §8), in the suite's anchor order — blue first,
|
|
7
|
+
* amber second, teal third, then green/red/navy/olive/slate — with no
|
|
8
|
+
* pink and no purple anywhere. It is the same palette the mermaid pie
|
|
9
|
+
* used before it delegated here, so a pie renders byte-identically
|
|
10
|
+
* through either package.
|
|
11
|
+
*
|
|
12
|
+
* The semantic tokens (text/grid/axis, the win/loss pair) resolve
|
|
13
|
+
* through the shared `resolveTheme` kernel with the `chart` prefix; the
|
|
14
|
+
* `'host'` theme links them to the site token vocabulary (docs/DESIGN.md §2,
|
|
15
|
+
* §7) so charts follow the host's light/dark flip live with no
|
|
16
|
+
* re-render. Sync invariant: the `default`/`dark` values below must
|
|
17
|
+
* match the `--chart-*` fallbacks in `styles/charts.css`.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { resolveTheme } from '@jarenjs/view/helpers';
|
|
21
|
+
import { inkFor, lerpColor } from '@jarenjs/core/color';
|
|
22
|
+
|
|
23
|
+
// Re-exported: chart modules have always imported it from here.
|
|
24
|
+
export { inkFor };
|
|
25
|
+
import { clamp01 } from '@jarenjs/core/math';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The categorical series palette (docs/DESIGN.md §8 anchor order).
|
|
29
|
+
* @type {readonly string[]}
|
|
30
|
+
*/
|
|
31
|
+
export const CATEGORICAL = ['#2563eb', '#f59e0b', '#0d9488', '#dc2626', '#16a34a', '#0369a1', '#ca8a04', '#64748b', '#93c5fd', '#78350f'];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Ordinal color assignment: series `i` gets the `i`-th palette entry,
|
|
35
|
+
* wrapping.
|
|
36
|
+
* @param {number} i - Series index
|
|
37
|
+
* @param {readonly string[]} [palette] - Palette to draw from
|
|
38
|
+
* @returns {string}
|
|
39
|
+
*/
|
|
40
|
+
export function seriesColor(i, palette = CATEGORICAL) {
|
|
41
|
+
return palette[i % palette.length];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The sequential magnitude ramp: one hue (the brand blue family),
|
|
46
|
+
* light→dark, for value-carrying fills (heatmap cells). A concrete
|
|
47
|
+
* constant like {@link CATEGORICAL}, not a theme token. The stops keep
|
|
48
|
+
* monotone perceptual lightness with visible step gaps, and both ends
|
|
49
|
+
* stay legible against the light and the dark site surface (the ramp
|
|
50
|
+
* does not flip with the theme).
|
|
51
|
+
* @type {readonly string[]}
|
|
52
|
+
*/
|
|
53
|
+
export const SEQUENTIAL = ['#60a5fa', '#3b82f6', '#2563eb', '#1e40af'];
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Continuous color for a normalized magnitude: `t` in [0,1] maps onto
|
|
57
|
+
* the ramp by piecewise-linear interpolation between its stops
|
|
58
|
+
* (clamped; non-finite `t` reads as 0).
|
|
59
|
+
* @param {number} t - Normalized magnitude (0 = low, 1 = high)
|
|
60
|
+
* @param {readonly string[]} [ramp] - Ramp stops, light→dark
|
|
61
|
+
* @returns {string} a `#rrggbb` color
|
|
62
|
+
*/
|
|
63
|
+
export function sequentialColor(t, ramp = SEQUENTIAL) {
|
|
64
|
+
// The finite guard is this ramp's own policy: a NaN magnitude reads as the
|
|
65
|
+
// low end rather than propagating, which `clamp01` deliberately does not do.
|
|
66
|
+
t = Number.isFinite(t) ? clamp01(t) : 0;
|
|
67
|
+
const spans = ramp.length - 1;
|
|
68
|
+
if (spans <= 0) return ramp[0];
|
|
69
|
+
const at = t * spans;
|
|
70
|
+
const i = Math.min(spans - 1, Math.floor(at));
|
|
71
|
+
return lerpColor(ramp[i], ramp[i + 1], at - i);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** @type {Record<string, Record<string, string>>} */
|
|
75
|
+
const THEMES = {
|
|
76
|
+
default: {
|
|
77
|
+
background: 'transparent',
|
|
78
|
+
text: '#1f2020',
|
|
79
|
+
muted: '#64748b',
|
|
80
|
+
grid: '#e2e8f0',
|
|
81
|
+
axis: '#94a3b8',
|
|
82
|
+
win: '#16a34a',
|
|
83
|
+
loss: '#dc2626',
|
|
84
|
+
sliceStroke: '#ffffff',
|
|
85
|
+
fontFamily: 'Inter, "Helvetica Neue", Arial, sans-serif',
|
|
86
|
+
},
|
|
87
|
+
dark: {
|
|
88
|
+
background: 'transparent',
|
|
89
|
+
text: '#f4f4f4',
|
|
90
|
+
muted: '#94a3b8',
|
|
91
|
+
grid: '#334155',
|
|
92
|
+
axis: '#64748b',
|
|
93
|
+
win: '#4ade80',
|
|
94
|
+
loss: '#f87171',
|
|
95
|
+
sliceStroke: '#11141c',
|
|
96
|
+
fontFamily: 'Inter, "Helvetica Neue", Arial, sans-serif',
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Host custom-property links for the `'host'` theme: token key → the
|
|
102
|
+
* site token it follows (docs/DESIGN.md §2). Concrete defaults remain as
|
|
103
|
+
* `var()` fallbacks, so the same SVG stays standalone-valid.
|
|
104
|
+
* @type {Record<string, string>}
|
|
105
|
+
*/
|
|
106
|
+
export const HOST_VARS = {
|
|
107
|
+
text: '--fg',
|
|
108
|
+
muted: '--muted',
|
|
109
|
+
grid: '--border',
|
|
110
|
+
axis: '--muted',
|
|
111
|
+
win: '--ok',
|
|
112
|
+
loss: '--fail',
|
|
113
|
+
sliceStroke: '--bg',
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Resolve a chart theme. The name `'host'` resolves the default tokens
|
|
118
|
+
* linked to the host token vocabulary via {@link HOST_VARS}.
|
|
119
|
+
* @param {string | Record<string, any>} [nameOrOverrides]
|
|
120
|
+
* @returns {{ name: string, tokens: Record<string, string>, cssVars: Record<string, string> }}
|
|
121
|
+
*/
|
|
122
|
+
export function createTheme(nameOrOverrides = 'default') {
|
|
123
|
+
return resolveTheme(THEMES, 'chart', nameOrOverrides, HOST_VARS);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export { THEMES };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Scales: pure `domain -> (value) => [0,1]` closures. All pixel
|
|
4
|
+
* mapping happens in the render pass; the AST and these scales stay in
|
|
5
|
+
* abstract unit space (the geometry-free contract). Construction may
|
|
6
|
+
* allocate (lookup maps); the returned functions never do.
|
|
7
|
+
*
|
|
8
|
+
* Out-of-domain inputs map outside [0,1] (linear/log/time) or to `NaN`
|
|
9
|
+
* (unknown ordinal/band categories); `polylinePath` and `num` in
|
|
10
|
+
* `@jarenjs/view/helpers` are the render-side guards that keep a NaN
|
|
11
|
+
* from ever reaching an emitted string.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {(value: number) => number} UnitScale
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Linear scale. A zero-span domain maps every value to 0.5.
|
|
20
|
+
* @param {number} min - Domain minimum
|
|
21
|
+
* @param {number} max - Domain maximum
|
|
22
|
+
* @returns {UnitScale}
|
|
23
|
+
*/
|
|
24
|
+
export function scaleLinear(min, max) {
|
|
25
|
+
const span = max - min;
|
|
26
|
+
if (span === 0)
|
|
27
|
+
return () => 0.5;
|
|
28
|
+
return (v) => (v - min) / span;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Logarithmic scale (base 10). The domain must be strictly positive —
|
|
33
|
+
* that is checked once here, not per call; a non-positive *value* maps
|
|
34
|
+
* to NaN (log of a non-positive number), which the render guards drop.
|
|
35
|
+
* @param {number} min - Domain minimum (> 0)
|
|
36
|
+
* @param {number} max - Domain maximum (> 0)
|
|
37
|
+
* @returns {UnitScale}
|
|
38
|
+
*/
|
|
39
|
+
export function scaleLog(min, max) {
|
|
40
|
+
if (!(min > 0) || !(max > 0))
|
|
41
|
+
throw new RangeError(`scaleLog domain must be positive, got [${min}, ${max}]`);
|
|
42
|
+
const logMin = Math.log10(min);
|
|
43
|
+
const span = Math.log10(max) - logMin;
|
|
44
|
+
if (span === 0)
|
|
45
|
+
return () => 0.5;
|
|
46
|
+
return (v) => (Math.log10(v) - logMin) / span;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Ordinal scale: each category maps to the center of its equal slot,
|
|
51
|
+
* `(i + 0.5) / n`. Unknown categories map to NaN.
|
|
52
|
+
* @param {readonly string[]} categories - Domain, in display order
|
|
53
|
+
* @returns {(category: string) => number}
|
|
54
|
+
*/
|
|
55
|
+
export function scaleOrdinal(categories) {
|
|
56
|
+
const n = categories.length;
|
|
57
|
+
const index = new Map();
|
|
58
|
+
for (let i = 0; i < n; ++i)
|
|
59
|
+
index.set(categories[i], i);
|
|
60
|
+
return (category) => {
|
|
61
|
+
const i = index.get(category);
|
|
62
|
+
return i === undefined ? NaN : (i + 0.5) / n;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Band scale: each category owns an equal band with inner padding. The
|
|
68
|
+
* returned function gives the band's start; `bandwidth` and `step` are
|
|
69
|
+
* exposed as properties. Unknown categories map to NaN.
|
|
70
|
+
* @param {readonly string[]} categories - Domain, in display order
|
|
71
|
+
* @param {number} [padding] - Fraction of each step left empty (0..1)
|
|
72
|
+
* @returns {((category: string) => number) & {bandwidth: number, step: number}}
|
|
73
|
+
*/
|
|
74
|
+
export function scaleBand(categories, padding = 0.2) {
|
|
75
|
+
const n = categories.length;
|
|
76
|
+
const step = n === 0 ? 1 : 1 / n;
|
|
77
|
+
const bandwidth = step * (1 - padding);
|
|
78
|
+
const inset = (step - bandwidth) / 2;
|
|
79
|
+
const index = new Map();
|
|
80
|
+
for (let i = 0; i < n; ++i)
|
|
81
|
+
index.set(categories[i], i);
|
|
82
|
+
const scale = (category) => {
|
|
83
|
+
const i = index.get(category);
|
|
84
|
+
return i === undefined ? NaN : i * step + inset;
|
|
85
|
+
};
|
|
86
|
+
scale.bandwidth = bandwidth;
|
|
87
|
+
scale.step = step;
|
|
88
|
+
return scale;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Time scale: a linear scale over epoch milliseconds that also accepts
|
|
93
|
+
* `Date` instances (converted once per call via `getTime`, no
|
|
94
|
+
* allocation).
|
|
95
|
+
* @param {number|Date} min - Domain minimum
|
|
96
|
+
* @param {number|Date} max - Domain maximum
|
|
97
|
+
* @returns {(value: number|Date) => number}
|
|
98
|
+
*/
|
|
99
|
+
export function scaleTime(min, max) {
|
|
100
|
+
const t0 = typeof min === 'number' ? min : min.getTime();
|
|
101
|
+
const t1 = typeof max === 'number' ? max : max.getTime();
|
|
102
|
+
const span = t1 - t0;
|
|
103
|
+
if (span === 0)
|
|
104
|
+
return () => 0.5;
|
|
105
|
+
return (v) => ((typeof v === 'number' ? v : v.getTime()) - t0) / span;
|
|
106
|
+
}
|
|
Binary file
|