@rokkit/chart 1.7.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/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/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/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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { buildUnifiedXScale, buildUnifiedYScale } from '../lib/plot/scales.js'
|
|
2
|
+
import { sortedBandDomain, resolveValueDomain } from '../lib/plot/domains.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The x and y position scales.
|
|
6
|
+
*
|
|
7
|
+
* Domains come, in order, from: the caller's override; for the value axis, the geometry of the
|
|
8
|
+
* mounted geoms (`resolveValueDomain` — box whiskers, stacked totals, running totals); then the
|
|
9
|
+
* extent of every geom's rows. Ranges come from the frame, oriented by the flip.
|
|
10
|
+
*/
|
|
11
|
+
export class ScaleState {
|
|
12
|
+
#config
|
|
13
|
+
#geoms
|
|
14
|
+
#channels
|
|
15
|
+
#orientation
|
|
16
|
+
#frame
|
|
17
|
+
#interaction
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {{
|
|
21
|
+
* config: import('./PlotConfig.svelte.js').PlotConfig,
|
|
22
|
+
* geoms: import('./GeomRegistry.svelte.js').GeomRegistry,
|
|
23
|
+
* channels: import('./ChannelState.svelte.js').ChannelState,
|
|
24
|
+
* orientation: import('./OrientationState.svelte.js').OrientationState,
|
|
25
|
+
* frame: import('./PlotFrame.svelte.js').PlotFrame,
|
|
26
|
+
* interaction: import('./InteractionState.svelte.js').InteractionState
|
|
27
|
+
* }} parts
|
|
28
|
+
*/
|
|
29
|
+
constructor({ config, geoms, channels, orientation, frame, interaction }) {
|
|
30
|
+
this.#config = config
|
|
31
|
+
this.#geoms = geoms
|
|
32
|
+
this.#channels = channels
|
|
33
|
+
this.#orientation = orientation
|
|
34
|
+
this.#frame = frame
|
|
35
|
+
this.#interaction = interaction
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Every geom's rows (post-stat) — or the raw data before any geom mounts. Shared by both axes. */
|
|
39
|
+
#datasets = $derived(
|
|
40
|
+
this.#geoms.list.length > 0
|
|
41
|
+
? this.#geoms.list.map((g) => this.#geoms.data(g.id))
|
|
42
|
+
: [this.#config.data]
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* x is a band when a banding geom is mounted — except a CONTINUOUS category axis (a bar
|
|
47
|
+
* race's tweened rank), which stays linear so fractional positions tween.
|
|
48
|
+
*/
|
|
49
|
+
#bandX = $derived(
|
|
50
|
+
this.#orientation.hasBandGeom &&
|
|
51
|
+
!this.#config.continuousCategory &&
|
|
52
|
+
(this.#orientation.orientation !== 'horizontal' || this.#orientation.flipped)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
/** The caller's x domain, else — on a band axis — categories sorted by value when asked. */
|
|
56
|
+
#xDomain = $derived(
|
|
57
|
+
this.#config.xDomain ??
|
|
58
|
+
(this.#bandX
|
|
59
|
+
? sortedBandDomain(
|
|
60
|
+
this.#datasets,
|
|
61
|
+
this.#channels.effective.x,
|
|
62
|
+
this.#channels.effective.y,
|
|
63
|
+
this.#config.sort
|
|
64
|
+
)
|
|
65
|
+
: null) ??
|
|
66
|
+
undefined
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
/** The x (category, when banded) scale. Includes zero only when x is the VALUE axis. */
|
|
70
|
+
x = $derived.by(() => {
|
|
71
|
+
const field = this.#channels.effective.x
|
|
72
|
+
if (!field) return null
|
|
73
|
+
const { orientation, flipped } = this.#orientation
|
|
74
|
+
const base = buildUnifiedXScale(this.#datasets, field, this.#frame.innerWidth, {
|
|
75
|
+
domain: this.#xDomain,
|
|
76
|
+
includeZero: orientation === 'horizontal' && !flipped,
|
|
77
|
+
band: this.#bandX,
|
|
78
|
+
// Flipped: the category axis stands up the screen.
|
|
79
|
+
range: flipped ? [this.#frame.innerHeight, 0] : undefined,
|
|
80
|
+
// A continuous category axis is deliberately padded — do not nice() it to whole numbers.
|
|
81
|
+
nice: !this.#config.continuousCategory
|
|
82
|
+
})
|
|
83
|
+
// A band cannot be zoomed; a continuous axis rescales through the transform.
|
|
84
|
+
return typeof base?.bandwidth === 'function' ? base : this.#zoomed(base, 'rescaleX')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
/** The y (value) scale — a zero baseline whenever it is the value axis. */
|
|
88
|
+
y = $derived.by(() => {
|
|
89
|
+
const field = this.#channels.effective.y
|
|
90
|
+
if (!field) return null
|
|
91
|
+
const { orientation, flipped } = this.#orientation
|
|
92
|
+
const geomDomain = resolveValueDomain(
|
|
93
|
+
this.#geoms.list,
|
|
94
|
+
(id) => this.#geoms.data(id),
|
|
95
|
+
this.#channels.effective,
|
|
96
|
+
field
|
|
97
|
+
)
|
|
98
|
+
const base = buildUnifiedYScale(this.#datasets, field, this.#frame.innerHeight, {
|
|
99
|
+
domain: this.#config.yDomain ?? geomDomain ?? undefined,
|
|
100
|
+
includeZero: orientation === 'vertical' || flipped,
|
|
101
|
+
// Flipped: the value axis runs along the screen.
|
|
102
|
+
range: flipped ? [0, this.#frame.innerWidth] : undefined
|
|
103
|
+
})
|
|
104
|
+
return this.#zoomed(base, 'rescaleY')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
/** @param {any} base @param {'rescaleX' | 'rescaleY'} method */
|
|
108
|
+
#zoomed(base, method) {
|
|
109
|
+
const zoom = this.#interaction.zoom
|
|
110
|
+
return zoom ? zoom[method](base) : base
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The categorical scale, whichever screen axis it is on. */
|
|
114
|
+
get band() {
|
|
115
|
+
return this.#orientation.bandIsX ? this.x : this.y
|
|
116
|
+
}
|
|
117
|
+
/** The continuous (value) scale, whichever screen axis it is on. */
|
|
118
|
+
get value() {
|
|
119
|
+
return this.#orientation.bandIsX ? this.y : this.x
|
|
120
|
+
}
|
|
121
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rokkit/chart",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Data-driven chart components",
|
|
6
6
|
"repository": {
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"@rokkit/states": "latest",
|
|
49
49
|
"d3-array": "^3.2.4",
|
|
50
50
|
"d3-axis": "^3.0.0",
|
|
51
|
+
"d3-contour": "^4.0.2",
|
|
51
52
|
"d3-format": "^3.1.2",
|
|
52
53
|
"d3-scale": "^4.0.2",
|
|
53
54
|
"d3-scale-chromatic": "^3.1.0",
|
|
@@ -65,6 +66,7 @@
|
|
|
65
66
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
|
66
67
|
"@types/d3-array": "^3.2.2",
|
|
67
68
|
"@types/d3-axis": "^3.0.6",
|
|
69
|
+
"@types/d3-contour": "^3.0.6",
|
|
68
70
|
"@types/d3-format": "^3.0.4",
|
|
69
71
|
"@types/d3-scale": "^4.0.9",
|
|
70
72
|
"@types/d3-scale-chromatic": "^3.1.0",
|
|
@@ -73,7 +75,7 @@
|
|
|
73
75
|
"@types/d3-transition": "^3.0.9",
|
|
74
76
|
"@types/d3-zoom": "^3.0.8",
|
|
75
77
|
"@types/ramda": "^0.31.1",
|
|
76
|
-
"svelte
|
|
77
|
-
"svelte": "^
|
|
78
|
+
"svelte": "^5.55.7",
|
|
79
|
+
"svelte-check": "^4.7.6"
|
|
78
80
|
}
|
|
79
81
|
}
|