@rokkit/chart 1.6.0 → 1.8.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 +13 -0
- package/dist/Plot.svelte +38 -10
- package/dist/Plot.svelte.d.ts +7 -2
- package/dist/PlotState.svelte.d.ts +115 -71
- package/dist/PlotState.svelte.js +174 -547
- package/dist/SparkState.svelte.d.ts +3 -1
- package/dist/SparkState.svelte.js +11 -41
- package/dist/geoms/Contour.svelte +88 -0
- package/dist/geoms/Contour.svelte.d.ts +19 -0
- package/dist/geoms/Hull.svelte +89 -0
- package/dist/geoms/Hull.svelte.d.ts +17 -0
- package/dist/geoms/Region.svelte +107 -0
- package/dist/geoms/Region.svelte.d.ts +27 -0
- package/dist/geoms/Rule.svelte +88 -26
- package/dist/geoms/Rule.svelte.d.ts +8 -0
- package/dist/geoms/lib/PlotAreaClip.svelte +18 -0
- package/dist/geoms/lib/PlotAreaClip.svelte.d.ts +6 -0
- package/dist/geoms/lib/bars.js +13 -20
- package/dist/geoms/lib/grouping.d.ts +35 -0
- package/dist/geoms/lib/grouping.js +59 -0
- package/dist/geoms/lib/marks/contour.d.ts +26 -0
- package/dist/geoms/lib/marks/contour.js +67 -0
- package/dist/geoms/lib/marks/hull.d.ts +26 -0
- package/dist/geoms/lib/marks/hull.js +36 -0
- package/dist/geoms/lib/marks/waterfall.js +15 -20
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/lib/abline.d.ts +41 -0
- package/dist/lib/abline.js +51 -0
- package/dist/lib/brewing/colors.js +1 -1
- package/dist/lib/hull.d.ts +18 -0
- package/dist/lib/hull.js +64 -0
- package/dist/lib/plot/domains.d.ts +61 -0
- package/dist/lib/plot/domains.js +130 -0
- package/dist/lib/plot/preset.d.ts +1 -1
- package/dist/lib/plot/preset.js +1 -1
- package/dist/lib/plot/running.d.ts +23 -0
- package/dist/lib/plot/running.js +32 -0
- package/dist/lib/plot/stacking.d.ts +32 -0
- package/dist/lib/plot/stacking.js +34 -0
- package/dist/lib/plot/types.d.ts +8 -1
- package/dist/lib/plot/types.js +5 -1
- package/dist/lib/preset.d.ts +2 -0
- package/dist/lib/preset.js +76 -41
- package/dist/lib/region.d.ts +63 -0
- package/dist/lib/region.js +127 -0
- package/dist/lib/swatch.d.ts +1 -1
- package/dist/lib/swatch.js +1 -1
- package/dist/state/AestheticState.svelte.d.ts +43 -0
- package/dist/state/AestheticState.svelte.js +84 -0
- package/dist/state/AxisState.svelte.d.ts +26 -0
- package/dist/state/AxisState.svelte.js +56 -0
- package/dist/state/ChannelState.svelte.d.ts +34 -0
- package/dist/state/ChannelState.svelte.js +67 -0
- package/dist/state/GeomRegistry.svelte.d.ts +44 -0
- package/dist/state/GeomRegistry.svelte.js +86 -0
- package/dist/state/InteractionState.svelte.d.ts +30 -0
- package/dist/state/InteractionState.svelte.js +69 -0
- package/dist/state/OrientationState.svelte.d.ts +34 -0
- package/dist/state/OrientationState.svelte.js +64 -0
- package/dist/state/PlotConfig.svelte.d.ts +179 -0
- package/dist/state/PlotConfig.svelte.js +195 -0
- package/dist/state/PlotFrame.svelte.d.ts +17 -0
- package/dist/state/PlotFrame.svelte.js +18 -0
- package/dist/state/ScaleState.svelte.d.ts +36 -0
- package/dist/state/ScaleState.svelte.js +121 -0
- package/package.json +5 -3
- package/dist/lib/palette.json +0 -277
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { SvelteSet } from 'svelte/reactivity'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What the reader is doing to the chart: the hovered row, the selected rows, the zoom.
|
|
5
|
+
*
|
|
6
|
+
* The selection is a set of ROW objects, compared by identity — they must be the rows the
|
|
7
|
+
* geoms were handed (`PlotState.data`), which is why the plot keeps one proxy for its data.
|
|
8
|
+
*/
|
|
9
|
+
export class InteractionState {
|
|
10
|
+
#config
|
|
11
|
+
#hovered = $state(null)
|
|
12
|
+
#selected = $state(new SvelteSet())
|
|
13
|
+
#zoom = $state(null)
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {import('./PlotConfig.svelte.js').PlotConfig} config
|
|
17
|
+
* @param {unknown[]} [selected] - the initial selection; afterwards the caller drives it
|
|
18
|
+
* through `setSelected` (PlotSurface syncs its bindable prop that way)
|
|
19
|
+
*/
|
|
20
|
+
constructor(config, selected) {
|
|
21
|
+
this.#config = config
|
|
22
|
+
if (selected) this.#selected = new SvelteSet(selected)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get hovered() {
|
|
26
|
+
return this.#hovered
|
|
27
|
+
}
|
|
28
|
+
setHovered(row) {
|
|
29
|
+
this.#hovered = row
|
|
30
|
+
}
|
|
31
|
+
clearHovered() {
|
|
32
|
+
this.#hovered = null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Marks are clickable: the caller listens, or selection is on. */
|
|
36
|
+
get interactive() {
|
|
37
|
+
return Boolean(this.#config.onselect) || this.#config.selectable
|
|
38
|
+
}
|
|
39
|
+
get selectedRows() {
|
|
40
|
+
return [...this.#selected]
|
|
41
|
+
}
|
|
42
|
+
isSelected(row) {
|
|
43
|
+
return this.#selected.has(row)
|
|
44
|
+
}
|
|
45
|
+
setSelected(rows) {
|
|
46
|
+
this.#selected = new SvelteSet(rows ?? [])
|
|
47
|
+
}
|
|
48
|
+
clearSelected() {
|
|
49
|
+
this.#selected = new SvelteSet()
|
|
50
|
+
}
|
|
51
|
+
/** An activation: always reported; toggles the row in the selection when selectable. */
|
|
52
|
+
handleSelect(detail) {
|
|
53
|
+
this.#config.onselect?.(detail)
|
|
54
|
+
if (!this.#config.selectable || detail?.datum === undefined) return
|
|
55
|
+
if (this.#selected.has(detail.datum)) this.#selected.delete(detail.datum)
|
|
56
|
+
else this.#selected.add(detail.datum)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The d3-zoom transform, or null at rest. Scales rescale through it. */
|
|
60
|
+
get zoom() {
|
|
61
|
+
return this.#zoom
|
|
62
|
+
}
|
|
63
|
+
applyZoom(transform) {
|
|
64
|
+
this.#zoom = transform
|
|
65
|
+
}
|
|
66
|
+
resetZoom() {
|
|
67
|
+
this.#zoom = null
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Geoms that band the x channel even when it is numeric (a year, a week, a race rank). */
|
|
2
|
+
export const CATEGORICAL_X: readonly string[];
|
|
3
|
+
/**
|
|
4
|
+
* Which way the chart reads, and how abstract (x-channel, y-channel) positions map to screen.
|
|
5
|
+
*
|
|
6
|
+
* Only a category-on-x chart flips: horizontal stands the band axis up the screen and lays the
|
|
7
|
+
* value axis along it, with the x/y channels unchanged. A plain scatter has no band, so
|
|
8
|
+
* `orientation` is a no-op for it.
|
|
9
|
+
*/
|
|
10
|
+
export class OrientationState {
|
|
11
|
+
/**
|
|
12
|
+
* @param {import('./PlotConfig.svelte.js').PlotConfig} config
|
|
13
|
+
* @param {import('./ChannelState.svelte.js').ChannelState} channels
|
|
14
|
+
* @param {import('./GeomRegistry.svelte.js').GeomRegistry} geoms
|
|
15
|
+
*/
|
|
16
|
+
constructor(config: import("./PlotConfig.svelte.js").PlotConfig, channels: import("./ChannelState.svelte.js").ChannelState, geoms: import("./GeomRegistry.svelte.js").GeomRegistry);
|
|
17
|
+
/** A mounted geom bands the x channel. */
|
|
18
|
+
hasBandGeom: boolean;
|
|
19
|
+
/** The caller's override, else inferred: 'vertical' | 'horizontal' | 'none'. */
|
|
20
|
+
orientation: string;
|
|
21
|
+
/** The x channel is the categorical (band) axis. */
|
|
22
|
+
bandIsX: boolean;
|
|
23
|
+
/** Rendered horizontally: the band axis stands up the screen. */
|
|
24
|
+
flipped: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Abstract (x-channel, y-channel) scale outputs to screen coordinates. Geoms compute
|
|
27
|
+
* `u = xScale(d[x])`, `v = yScale(d[y])`, then place them.
|
|
28
|
+
*/
|
|
29
|
+
place(u: any, v: any): {
|
|
30
|
+
x: any;
|
|
31
|
+
y: any;
|
|
32
|
+
};
|
|
33
|
+
#private;
|
|
34
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { inferFieldType, inferOrientation } from '../lib/plot/scales.js'
|
|
2
|
+
|
|
3
|
+
/** Geoms that band the x channel even when it is numeric (a year, a week, a race rank). */
|
|
4
|
+
export const CATEGORICAL_X = Object.freeze(['bar', 'box', 'violin', 'jitter'])
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Which way the chart reads, and how abstract (x-channel, y-channel) positions map to screen.
|
|
8
|
+
*
|
|
9
|
+
* Only a category-on-x chart flips: horizontal stands the band axis up the screen and lays the
|
|
10
|
+
* value axis along it, with the x/y channels unchanged. A plain scatter has no band, so
|
|
11
|
+
* `orientation` is a no-op for it.
|
|
12
|
+
*/
|
|
13
|
+
export class OrientationState {
|
|
14
|
+
#config
|
|
15
|
+
#channels
|
|
16
|
+
#geoms
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {import('./PlotConfig.svelte.js').PlotConfig} config
|
|
20
|
+
* @param {import('./ChannelState.svelte.js').ChannelState} channels
|
|
21
|
+
* @param {import('./GeomRegistry.svelte.js').GeomRegistry} geoms
|
|
22
|
+
*/
|
|
23
|
+
constructor(config, channels, geoms) {
|
|
24
|
+
this.#config = config
|
|
25
|
+
this.#channels = channels
|
|
26
|
+
this.#geoms = geoms
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A mounted geom bands the x channel. */
|
|
30
|
+
hasBandGeom = $derived(this.#geoms.list.some((g) => CATEGORICAL_X.includes(g.type)))
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Field types of the effective x and y, with x forced to `band` when a banding geom sits on
|
|
34
|
+
* a continuous x/y pair. Null until both channels exist. Computed once for orientation and
|
|
35
|
+
* `bandIsX` both — they used to infer the same types twice.
|
|
36
|
+
*/
|
|
37
|
+
#types = $derived.by(() => {
|
|
38
|
+
const { x, y } = this.#channels.effective
|
|
39
|
+
if (!x || !y) return null
|
|
40
|
+
const rawX = inferFieldType(this.#config.data, x)
|
|
41
|
+
const yType = inferFieldType(this.#config.data, y)
|
|
42
|
+
const xType =
|
|
43
|
+
this.hasBandGeom && rawX === 'continuous' && yType === 'continuous' ? 'band' : rawX
|
|
44
|
+
return { xType, yType }
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
/** The caller's override, else inferred: 'vertical' | 'horizontal' | 'none'. */
|
|
48
|
+
orientation = $derived(
|
|
49
|
+
this.#config.orientation ??
|
|
50
|
+
(this.#types ? inferOrientation(this.#types.xType, this.#types.yType) : 'none')
|
|
51
|
+
)
|
|
52
|
+
/** The x channel is the categorical (band) axis. */
|
|
53
|
+
bandIsX = $derived(this.#types?.xType === 'band')
|
|
54
|
+
/** Rendered horizontally: the band axis stands up the screen. */
|
|
55
|
+
flipped = $derived(this.orientation === 'horizontal' && this.bandIsX)
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Abstract (x-channel, y-channel) scale outputs to screen coordinates. Geoms compute
|
|
59
|
+
* `u = xScale(d[x])`, `v = yScale(d[y])`, then place them.
|
|
60
|
+
*/
|
|
61
|
+
place(u, v) {
|
|
62
|
+
return this.flipped ? { x: v, y: u } : { x: u, y: v }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/** The frame's margin when the caller sets none. */
|
|
2
|
+
export const DEFAULT_MARGIN: Readonly<{
|
|
3
|
+
top: 20;
|
|
4
|
+
right: 20;
|
|
5
|
+
bottom: 40;
|
|
6
|
+
left: 50;
|
|
7
|
+
}>;
|
|
8
|
+
/**
|
|
9
|
+
* Every input a plot takes, and what `update()` does when a field is omitted.
|
|
10
|
+
*
|
|
11
|
+
* - `keep`: the previous value stays. These are the chart's data and settings; a partial
|
|
12
|
+
* update (`{ width }` after a resize) must not wipe the rest.
|
|
13
|
+
* - `reset`: the default comes back. These are OVERRIDES — a fixed domain, a forced
|
|
14
|
+
* orientation, a margin — and an override the caller stopped passing must stop applying.
|
|
15
|
+
*
|
|
16
|
+
* This table is the whole contract; `PlotState-update-contract.spec.js` pins it field by field.
|
|
17
|
+
*
|
|
18
|
+
* @type {ReadonlyArray<{ key: string, fallback: () => unknown, whenOmitted: 'keep' | 'reset' }>}
|
|
19
|
+
*/
|
|
20
|
+
export const CONFIG_FIELDS: ReadonlyArray<{
|
|
21
|
+
key: string;
|
|
22
|
+
fallback: () => unknown;
|
|
23
|
+
whenOmitted: "keep" | "reset";
|
|
24
|
+
}>;
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {Record<string, unknown>} Row
|
|
27
|
+
* @typedef {{ top: number, right: number, bottom: number, left: number }} Margin
|
|
28
|
+
* @typedef {{ x?: string, y?: string, color?: string, fill?: string, pattern?: string, symbol?: string, size?: string }} Channels
|
|
29
|
+
* @typedef {Object} PlotConfigValues
|
|
30
|
+
* @property {Row[]} data
|
|
31
|
+
* @property {Channels} channels
|
|
32
|
+
* @property {Record<string, string>} labels
|
|
33
|
+
* @property {import('../lib/plot/types.js').PlotHelpers} helpers
|
|
34
|
+
* @property {string | undefined} preset
|
|
35
|
+
* @property {number | undefined} colorMidpoint
|
|
36
|
+
* @property {string | undefined} colorScale
|
|
37
|
+
* @property {string | undefined} colorScheme
|
|
38
|
+
* @property {unknown[] | undefined} colorDomain
|
|
39
|
+
* @property {unknown[] | undefined} xDomain
|
|
40
|
+
* @property {unknown[] | undefined} yDomain
|
|
41
|
+
* @property {number} width
|
|
42
|
+
* @property {number} height
|
|
43
|
+
* @property {'light' | 'dark'} mode
|
|
44
|
+
* @property {typeof defaultPreset} chartPreset
|
|
45
|
+
* @property {((detail: unknown) => void) | undefined} onselect
|
|
46
|
+
* @property {boolean} selectable
|
|
47
|
+
* @property {number} axisOffset
|
|
48
|
+
* @property {[number | undefined, number | undefined]} axisOrigin
|
|
49
|
+
* @property {Margin | undefined} margin
|
|
50
|
+
* @property {'horizontal' | 'vertical' | undefined} orientation
|
|
51
|
+
* @property {boolean} continuousCategory
|
|
52
|
+
* @property {'asc' | 'desc' | undefined} sort
|
|
53
|
+
*/
|
|
54
|
+
/**
|
|
55
|
+
* The plot's inputs — the one job `update()` has. Every other state class reads from here and
|
|
56
|
+
* writes nothing back.
|
|
57
|
+
*
|
|
58
|
+
* One `$state` object rather than a field per input: Svelte proxies it per property, so each
|
|
59
|
+
* reader still tracks only the fields it touches, and the table above can drive construction
|
|
60
|
+
* and update without a hand-written line per field. Values are proxied deeply exactly as the
|
|
61
|
+
* per-field `$state`s before were, which is what row identity (`data.indexOf(row)`) relies on.
|
|
62
|
+
*/
|
|
63
|
+
export class PlotConfig {
|
|
64
|
+
/** @param {Record<string, unknown>} [config] */
|
|
65
|
+
constructor(config?: Record<string, unknown>);
|
|
66
|
+
/** @param {Record<string, unknown>} [config] */
|
|
67
|
+
update(config?: Record<string, unknown>): void;
|
|
68
|
+
get data(): Row[];
|
|
69
|
+
get channels(): Channels;
|
|
70
|
+
get helpers(): import("../lib/plot/types.js").PlotHelpers;
|
|
71
|
+
get colorMidpoint(): number | undefined;
|
|
72
|
+
get colorScale(): string | undefined;
|
|
73
|
+
get colorScheme(): string | undefined;
|
|
74
|
+
get colorDomain(): unknown[] | undefined;
|
|
75
|
+
get xDomain(): unknown[] | undefined;
|
|
76
|
+
get yDomain(): unknown[] | undefined;
|
|
77
|
+
get width(): number;
|
|
78
|
+
get height(): number;
|
|
79
|
+
get mode(): "light" | "dark";
|
|
80
|
+
get chartPreset(): {
|
|
81
|
+
colors: string[];
|
|
82
|
+
shades: {
|
|
83
|
+
light: {
|
|
84
|
+
fill: string;
|
|
85
|
+
stroke: string;
|
|
86
|
+
};
|
|
87
|
+
dark: {
|
|
88
|
+
fill: string;
|
|
89
|
+
stroke: string;
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
opacity: {
|
|
93
|
+
area: number;
|
|
94
|
+
bar: number;
|
|
95
|
+
line: number;
|
|
96
|
+
point: number;
|
|
97
|
+
arc: number;
|
|
98
|
+
box: number;
|
|
99
|
+
violin: number;
|
|
100
|
+
jitter: number;
|
|
101
|
+
heatmap: number;
|
|
102
|
+
candlestick: number;
|
|
103
|
+
hexbin: number;
|
|
104
|
+
ribbon: number;
|
|
105
|
+
waterfall: number;
|
|
106
|
+
rule: number;
|
|
107
|
+
hull: number;
|
|
108
|
+
contour: number;
|
|
109
|
+
radar: number;
|
|
110
|
+
};
|
|
111
|
+
patterns: string[];
|
|
112
|
+
symbols: string[];
|
|
113
|
+
};
|
|
114
|
+
get onselect(): ((detail: unknown) => void) | undefined;
|
|
115
|
+
get selectable(): boolean;
|
|
116
|
+
get axisOffset(): number;
|
|
117
|
+
/** Settable directly as well as through `update()` — callers (and specs) pin a crossing. */
|
|
118
|
+
set axisOrigin(value: [number | undefined, number | undefined]);
|
|
119
|
+
get axisOrigin(): [number | undefined, number | undefined];
|
|
120
|
+
get margin(): Margin | undefined;
|
|
121
|
+
get orientation(): "horizontal" | "vertical" | undefined;
|
|
122
|
+
get continuousCategory(): boolean;
|
|
123
|
+
get sort(): "asc" | "desc" | undefined;
|
|
124
|
+
/** @param {string} field */
|
|
125
|
+
label(field: string): string;
|
|
126
|
+
/** @param {string} field */
|
|
127
|
+
format(field: string): any;
|
|
128
|
+
tooltip(): any;
|
|
129
|
+
/** @param {string} type */
|
|
130
|
+
geomComponent(type: string): any;
|
|
131
|
+
resolvedPreset(): {
|
|
132
|
+
colors: any;
|
|
133
|
+
patterns: any;
|
|
134
|
+
symbols: any;
|
|
135
|
+
};
|
|
136
|
+
#private;
|
|
137
|
+
}
|
|
138
|
+
export type Row = Record<string, unknown>;
|
|
139
|
+
export type Margin = {
|
|
140
|
+
top: number;
|
|
141
|
+
right: number;
|
|
142
|
+
bottom: number;
|
|
143
|
+
left: number;
|
|
144
|
+
};
|
|
145
|
+
export type Channels = {
|
|
146
|
+
x?: string;
|
|
147
|
+
y?: string;
|
|
148
|
+
color?: string;
|
|
149
|
+
fill?: string;
|
|
150
|
+
pattern?: string;
|
|
151
|
+
symbol?: string;
|
|
152
|
+
size?: string;
|
|
153
|
+
};
|
|
154
|
+
export type PlotConfigValues = {
|
|
155
|
+
data: Row[];
|
|
156
|
+
channels: Channels;
|
|
157
|
+
labels: Record<string, string>;
|
|
158
|
+
helpers: import("../lib/plot/types.js").PlotHelpers;
|
|
159
|
+
preset: string | undefined;
|
|
160
|
+
colorMidpoint: number | undefined;
|
|
161
|
+
colorScale: string | undefined;
|
|
162
|
+
colorScheme: string | undefined;
|
|
163
|
+
colorDomain: unknown[] | undefined;
|
|
164
|
+
xDomain: unknown[] | undefined;
|
|
165
|
+
yDomain: unknown[] | undefined;
|
|
166
|
+
width: number;
|
|
167
|
+
height: number;
|
|
168
|
+
mode: "light" | "dark";
|
|
169
|
+
chartPreset: typeof defaultPreset;
|
|
170
|
+
onselect: ((detail: unknown) => void) | undefined;
|
|
171
|
+
selectable: boolean;
|
|
172
|
+
axisOffset: number;
|
|
173
|
+
axisOrigin: [number | undefined, number | undefined];
|
|
174
|
+
margin: Margin | undefined;
|
|
175
|
+
orientation: "horizontal" | "vertical" | undefined;
|
|
176
|
+
continuousCategory: boolean;
|
|
177
|
+
sort: "asc" | "desc" | undefined;
|
|
178
|
+
};
|
|
179
|
+
import { defaultPreset } from '../lib/preset.js';
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { defaultPreset } from '../lib/preset.js'
|
|
2
|
+
import { resolvePreset } from '../lib/plot/preset.js'
|
|
3
|
+
import { resolveFormat, resolveTooltip, resolveGeom } from '../lib/plot/helpers.js'
|
|
4
|
+
|
|
5
|
+
/** The frame's margin when the caller sets none. */
|
|
6
|
+
export const DEFAULT_MARGIN = Object.freeze({ top: 20, right: 20, bottom: 40, left: 50 })
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Every input a plot takes, and what `update()` does when a field is omitted.
|
|
10
|
+
*
|
|
11
|
+
* - `keep`: the previous value stays. These are the chart's data and settings; a partial
|
|
12
|
+
* update (`{ width }` after a resize) must not wipe the rest.
|
|
13
|
+
* - `reset`: the default comes back. These are OVERRIDES — a fixed domain, a forced
|
|
14
|
+
* orientation, a margin — and an override the caller stopped passing must stop applying.
|
|
15
|
+
*
|
|
16
|
+
* This table is the whole contract; `PlotState-update-contract.spec.js` pins it field by field.
|
|
17
|
+
*
|
|
18
|
+
* @type {ReadonlyArray<{ key: string, fallback: () => unknown, whenOmitted: 'keep' | 'reset' }>}
|
|
19
|
+
*/
|
|
20
|
+
export const CONFIG_FIELDS = Object.freeze([
|
|
21
|
+
{ key: 'data', fallback: () => [], whenOmitted: 'keep' },
|
|
22
|
+
{ key: 'channels', fallback: () => ({}), whenOmitted: 'keep' },
|
|
23
|
+
{ key: 'labels', fallback: () => ({}), whenOmitted: 'keep' },
|
|
24
|
+
{ key: 'helpers', fallback: () => ({}), whenOmitted: 'keep' },
|
|
25
|
+
{ key: 'preset', fallback: () => undefined, whenOmitted: 'keep' },
|
|
26
|
+
{ key: 'colorMidpoint', fallback: () => undefined, whenOmitted: 'keep' },
|
|
27
|
+
{ key: 'colorScale', fallback: () => undefined, whenOmitted: 'keep' },
|
|
28
|
+
{ key: 'colorScheme', fallback: () => undefined, whenOmitted: 'keep' },
|
|
29
|
+
{ key: 'colorDomain', fallback: () => undefined, whenOmitted: 'reset' },
|
|
30
|
+
{ key: 'xDomain', fallback: () => undefined, whenOmitted: 'reset' },
|
|
31
|
+
{ key: 'yDomain', fallback: () => undefined, whenOmitted: 'reset' },
|
|
32
|
+
{ key: 'width', fallback: () => 600, whenOmitted: 'keep' },
|
|
33
|
+
{ key: 'height', fallback: () => 400, whenOmitted: 'keep' },
|
|
34
|
+
{ key: 'mode', fallback: () => 'light', whenOmitted: 'keep' },
|
|
35
|
+
{ key: 'chartPreset', fallback: () => defaultPreset, whenOmitted: 'keep' },
|
|
36
|
+
{ key: 'onselect', fallback: () => undefined, whenOmitted: 'keep' },
|
|
37
|
+
{ key: 'selectable', fallback: () => false, whenOmitted: 'keep' },
|
|
38
|
+
{ key: 'axisOffset', fallback: () => 0, whenOmitted: 'keep' },
|
|
39
|
+
{ key: 'axisOrigin', fallback: () => [undefined, undefined], whenOmitted: 'reset' },
|
|
40
|
+
{ key: 'margin', fallback: () => undefined, whenOmitted: 'reset' },
|
|
41
|
+
{ key: 'orientation', fallback: () => undefined, whenOmitted: 'reset' },
|
|
42
|
+
{ key: 'continuousCategory', fallback: () => false, whenOmitted: 'reset' },
|
|
43
|
+
{ key: 'sort', fallback: () => undefined, whenOmitted: 'reset' }
|
|
44
|
+
])
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {Record<string, unknown>} Row
|
|
48
|
+
* @typedef {{ top: number, right: number, bottom: number, left: number }} Margin
|
|
49
|
+
* @typedef {{ x?: string, y?: string, color?: string, fill?: string, pattern?: string, symbol?: string, size?: string }} Channels
|
|
50
|
+
* @typedef {Object} PlotConfigValues
|
|
51
|
+
* @property {Row[]} data
|
|
52
|
+
* @property {Channels} channels
|
|
53
|
+
* @property {Record<string, string>} labels
|
|
54
|
+
* @property {import('../lib/plot/types.js').PlotHelpers} helpers
|
|
55
|
+
* @property {string | undefined} preset
|
|
56
|
+
* @property {number | undefined} colorMidpoint
|
|
57
|
+
* @property {string | undefined} colorScale
|
|
58
|
+
* @property {string | undefined} colorScheme
|
|
59
|
+
* @property {unknown[] | undefined} colorDomain
|
|
60
|
+
* @property {unknown[] | undefined} xDomain
|
|
61
|
+
* @property {unknown[] | undefined} yDomain
|
|
62
|
+
* @property {number} width
|
|
63
|
+
* @property {number} height
|
|
64
|
+
* @property {'light' | 'dark'} mode
|
|
65
|
+
* @property {typeof defaultPreset} chartPreset
|
|
66
|
+
* @property {((detail: unknown) => void) | undefined} onselect
|
|
67
|
+
* @property {boolean} selectable
|
|
68
|
+
* @property {number} axisOffset
|
|
69
|
+
* @property {[number | undefined, number | undefined]} axisOrigin
|
|
70
|
+
* @property {Margin | undefined} margin
|
|
71
|
+
* @property {'horizontal' | 'vertical' | undefined} orientation
|
|
72
|
+
* @property {boolean} continuousCategory
|
|
73
|
+
* @property {'asc' | 'desc' | undefined} sort
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The plot's inputs — the one job `update()` has. Every other state class reads from here and
|
|
78
|
+
* writes nothing back.
|
|
79
|
+
*
|
|
80
|
+
* One `$state` object rather than a field per input: Svelte proxies it per property, so each
|
|
81
|
+
* reader still tracks only the fields it touches, and the table above can drive construction
|
|
82
|
+
* and update without a hand-written line per field. Values are proxied deeply exactly as the
|
|
83
|
+
* per-field `$state`s before were, which is what row identity (`data.indexOf(row)`) relies on.
|
|
84
|
+
*/
|
|
85
|
+
export class PlotConfig {
|
|
86
|
+
/** @type {PlotConfigValues} */
|
|
87
|
+
#v = $state(
|
|
88
|
+
/** @type {PlotConfigValues} */ (
|
|
89
|
+
Object.fromEntries(CONFIG_FIELDS.map((f) => [f.key, f.fallback()]))
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
/** @param {Record<string, unknown>} [config] */
|
|
94
|
+
constructor(config = {}) {
|
|
95
|
+
const v = /** @type {Record<string, unknown>} */ (this.#v)
|
|
96
|
+
for (const f of CONFIG_FIELDS) v[f.key] = config[f.key] ?? f.fallback()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** @param {Record<string, unknown>} [config] */
|
|
100
|
+
update(config = {}) {
|
|
101
|
+
const v = /** @type {Record<string, unknown>} */ (this.#v)
|
|
102
|
+
for (const f of CONFIG_FIELDS) {
|
|
103
|
+
if (config[f.key] !== undefined) v[f.key] = config[f.key]
|
|
104
|
+
else if (f.whenOmitted === 'reset') v[f.key] = f.fallback()
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
get data() {
|
|
109
|
+
return this.#v.data
|
|
110
|
+
}
|
|
111
|
+
get channels() {
|
|
112
|
+
return this.#v.channels
|
|
113
|
+
}
|
|
114
|
+
get helpers() {
|
|
115
|
+
return this.#v.helpers
|
|
116
|
+
}
|
|
117
|
+
get colorMidpoint() {
|
|
118
|
+
return this.#v.colorMidpoint
|
|
119
|
+
}
|
|
120
|
+
get colorScale() {
|
|
121
|
+
return this.#v.colorScale
|
|
122
|
+
}
|
|
123
|
+
get colorScheme() {
|
|
124
|
+
return this.#v.colorScheme
|
|
125
|
+
}
|
|
126
|
+
get colorDomain() {
|
|
127
|
+
return this.#v.colorDomain
|
|
128
|
+
}
|
|
129
|
+
get xDomain() {
|
|
130
|
+
return this.#v.xDomain
|
|
131
|
+
}
|
|
132
|
+
get yDomain() {
|
|
133
|
+
return this.#v.yDomain
|
|
134
|
+
}
|
|
135
|
+
get width() {
|
|
136
|
+
return this.#v.width
|
|
137
|
+
}
|
|
138
|
+
get height() {
|
|
139
|
+
return this.#v.height
|
|
140
|
+
}
|
|
141
|
+
get mode() {
|
|
142
|
+
return this.#v.mode
|
|
143
|
+
}
|
|
144
|
+
get chartPreset() {
|
|
145
|
+
return this.#v.chartPreset
|
|
146
|
+
}
|
|
147
|
+
get onselect() {
|
|
148
|
+
return this.#v.onselect
|
|
149
|
+
}
|
|
150
|
+
get selectable() {
|
|
151
|
+
return this.#v.selectable
|
|
152
|
+
}
|
|
153
|
+
get axisOffset() {
|
|
154
|
+
return this.#v.axisOffset
|
|
155
|
+
}
|
|
156
|
+
get axisOrigin() {
|
|
157
|
+
return this.#v.axisOrigin
|
|
158
|
+
}
|
|
159
|
+
/** Settable directly as well as through `update()` — callers (and specs) pin a crossing. */
|
|
160
|
+
set axisOrigin(value) {
|
|
161
|
+
this.#v.axisOrigin = value
|
|
162
|
+
}
|
|
163
|
+
get margin() {
|
|
164
|
+
return this.#v.margin
|
|
165
|
+
}
|
|
166
|
+
get orientation() {
|
|
167
|
+
return this.#v.orientation
|
|
168
|
+
}
|
|
169
|
+
get continuousCategory() {
|
|
170
|
+
return this.#v.continuousCategory
|
|
171
|
+
}
|
|
172
|
+
get sort() {
|
|
173
|
+
return this.#v.sort
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ─── Helper resolution — these read nothing but the inputs ─────────────────
|
|
177
|
+
/** @param {string} field */
|
|
178
|
+
label(field) {
|
|
179
|
+
return this.#v.labels?.[field] ?? field
|
|
180
|
+
}
|
|
181
|
+
/** @param {string} field */
|
|
182
|
+
format(field) {
|
|
183
|
+
return resolveFormat(field, this.#v.helpers)
|
|
184
|
+
}
|
|
185
|
+
tooltip() {
|
|
186
|
+
return resolveTooltip(this.#v.helpers)
|
|
187
|
+
}
|
|
188
|
+
/** @param {string} type */
|
|
189
|
+
geomComponent(type) {
|
|
190
|
+
return resolveGeom(type, this.#v.helpers)
|
|
191
|
+
}
|
|
192
|
+
resolvedPreset() {
|
|
193
|
+
return resolvePreset(this.#v.preset, this.#v.helpers)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The drawing area: the plot's size less its margin. Every scale's range comes from here.
|
|
3
|
+
*/
|
|
4
|
+
export class PlotFrame {
|
|
5
|
+
/** @param {import('./PlotConfig.svelte.js').PlotConfig} config */
|
|
6
|
+
constructor(config: import("./PlotConfig.svelte.js").PlotConfig);
|
|
7
|
+
/** The caller's margin override, else the default. */
|
|
8
|
+
margin: Readonly<{
|
|
9
|
+
top: 20;
|
|
10
|
+
right: 20;
|
|
11
|
+
bottom: 40;
|
|
12
|
+
left: 50;
|
|
13
|
+
}> | import("./PlotConfig.svelte.js").Margin;
|
|
14
|
+
innerWidth: number;
|
|
15
|
+
innerHeight: number;
|
|
16
|
+
#private;
|
|
17
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { DEFAULT_MARGIN } from './PlotConfig.svelte.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The drawing area: the plot's size less its margin. Every scale's range comes from here.
|
|
5
|
+
*/
|
|
6
|
+
export class PlotFrame {
|
|
7
|
+
#config
|
|
8
|
+
|
|
9
|
+
/** @param {import('./PlotConfig.svelte.js').PlotConfig} config */
|
|
10
|
+
constructor(config) {
|
|
11
|
+
this.#config = config
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The caller's margin override, else the default. */
|
|
15
|
+
margin = $derived(this.#config.margin ?? DEFAULT_MARGIN)
|
|
16
|
+
innerWidth = $derived(this.#config.width - this.margin.left - this.margin.right)
|
|
17
|
+
innerHeight = $derived(this.#config.height - this.margin.top - this.margin.bottom)
|
|
18
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The x and y position scales.
|
|
3
|
+
*
|
|
4
|
+
* Domains come, in order, from: the caller's override; for the value axis, the geometry of the
|
|
5
|
+
* mounted geoms (`resolveValueDomain` — box whiskers, stacked totals, running totals); then the
|
|
6
|
+
* extent of every geom's rows. Ranges come from the frame, oriented by the flip.
|
|
7
|
+
*/
|
|
8
|
+
export class ScaleState {
|
|
9
|
+
/**
|
|
10
|
+
* @param {{
|
|
11
|
+
* config: import('./PlotConfig.svelte.js').PlotConfig,
|
|
12
|
+
* geoms: import('./GeomRegistry.svelte.js').GeomRegistry,
|
|
13
|
+
* channels: import('./ChannelState.svelte.js').ChannelState,
|
|
14
|
+
* orientation: import('./OrientationState.svelte.js').OrientationState,
|
|
15
|
+
* frame: import('./PlotFrame.svelte.js').PlotFrame,
|
|
16
|
+
* interaction: import('./InteractionState.svelte.js').InteractionState
|
|
17
|
+
* }} parts
|
|
18
|
+
*/
|
|
19
|
+
constructor({ config, geoms, channels, orientation, frame, interaction }: {
|
|
20
|
+
config: import("./PlotConfig.svelte.js").PlotConfig;
|
|
21
|
+
geoms: import("./GeomRegistry.svelte.js").GeomRegistry;
|
|
22
|
+
channels: import("./ChannelState.svelte.js").ChannelState;
|
|
23
|
+
orientation: import("./OrientationState.svelte.js").OrientationState;
|
|
24
|
+
frame: import("./PlotFrame.svelte.js").PlotFrame;
|
|
25
|
+
interaction: import("./InteractionState.svelte.js").InteractionState;
|
|
26
|
+
});
|
|
27
|
+
/** The x (category, when banded) scale. Includes zero only when x is the VALUE axis. */
|
|
28
|
+
x: any;
|
|
29
|
+
/** The y (value) scale — a zero baseline whenever it is the value axis. */
|
|
30
|
+
y: any;
|
|
31
|
+
/** The categorical scale, whichever screen axis it is on. */
|
|
32
|
+
get band(): any;
|
|
33
|
+
/** The continuous (value) scale, whichever screen axis it is on. */
|
|
34
|
+
get value(): any;
|
|
35
|
+
#private;
|
|
36
|
+
}
|