@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.
Files changed (62) hide show
  1. package/README.md +13 -0
  2. package/dist/Plot.svelte +38 -10
  3. package/dist/Plot.svelte.d.ts +7 -2
  4. package/dist/PlotState.svelte.d.ts +115 -71
  5. package/dist/PlotState.svelte.js +174 -547
  6. package/dist/SparkState.svelte.d.ts +3 -1
  7. package/dist/SparkState.svelte.js +11 -41
  8. package/dist/geoms/Contour.svelte +88 -0
  9. package/dist/geoms/Contour.svelte.d.ts +19 -0
  10. package/dist/geoms/Hull.svelte +89 -0
  11. package/dist/geoms/Hull.svelte.d.ts +17 -0
  12. package/dist/geoms/Region.svelte +107 -0
  13. package/dist/geoms/Region.svelte.d.ts +27 -0
  14. package/dist/geoms/Rule.svelte +88 -26
  15. package/dist/geoms/Rule.svelte.d.ts +8 -0
  16. package/dist/geoms/lib/PlotAreaClip.svelte +18 -0
  17. package/dist/geoms/lib/PlotAreaClip.svelte.d.ts +6 -0
  18. package/dist/geoms/lib/bars.js +13 -20
  19. package/dist/geoms/lib/grouping.d.ts +35 -0
  20. package/dist/geoms/lib/grouping.js +59 -0
  21. package/dist/geoms/lib/marks/contour.d.ts +26 -0
  22. package/dist/geoms/lib/marks/contour.js +67 -0
  23. package/dist/geoms/lib/marks/hull.d.ts +26 -0
  24. package/dist/geoms/lib/marks/hull.js +36 -0
  25. package/dist/geoms/lib/marks/waterfall.js +15 -20
  26. package/dist/index.d.ts +9 -0
  27. package/dist/index.js +9 -0
  28. package/dist/lib/abline.d.ts +41 -0
  29. package/dist/lib/abline.js +51 -0
  30. package/dist/lib/hull.d.ts +18 -0
  31. package/dist/lib/hull.js +64 -0
  32. package/dist/lib/plot/domains.d.ts +61 -0
  33. package/dist/lib/plot/domains.js +130 -0
  34. package/dist/lib/plot/running.d.ts +23 -0
  35. package/dist/lib/plot/running.js +32 -0
  36. package/dist/lib/plot/stacking.d.ts +32 -0
  37. package/dist/lib/plot/stacking.js +34 -0
  38. package/dist/lib/plot/types.d.ts +8 -1
  39. package/dist/lib/plot/types.js +5 -1
  40. package/dist/lib/preset.d.ts +2 -0
  41. package/dist/lib/preset.js +76 -41
  42. package/dist/lib/region.d.ts +63 -0
  43. package/dist/lib/region.js +127 -0
  44. package/dist/state/AestheticState.svelte.d.ts +43 -0
  45. package/dist/state/AestheticState.svelte.js +84 -0
  46. package/dist/state/AxisState.svelte.d.ts +26 -0
  47. package/dist/state/AxisState.svelte.js +56 -0
  48. package/dist/state/ChannelState.svelte.d.ts +34 -0
  49. package/dist/state/ChannelState.svelte.js +67 -0
  50. package/dist/state/GeomRegistry.svelte.d.ts +44 -0
  51. package/dist/state/GeomRegistry.svelte.js +86 -0
  52. package/dist/state/InteractionState.svelte.d.ts +30 -0
  53. package/dist/state/InteractionState.svelte.js +69 -0
  54. package/dist/state/OrientationState.svelte.d.ts +34 -0
  55. package/dist/state/OrientationState.svelte.js +64 -0
  56. package/dist/state/PlotConfig.svelte.d.ts +179 -0
  57. package/dist/state/PlotConfig.svelte.js +195 -0
  58. package/dist/state/PlotFrame.svelte.d.ts +17 -0
  59. package/dist/state/PlotFrame.svelte.js +18 -0
  60. package/dist/state/ScaleState.svelte.d.ts +36 -0
  61. package/dist/state/ScaleState.svelte.js +121 -0
  62. 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.7.0",
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-check": "^4.7.6",
77
- "svelte": "^5.55.7"
78
+ "svelte": "^5.55.7",
79
+ "svelte-check": "^4.7.6"
78
80
  }
79
81
  }