svelteplot 0.10.3-pr-479.0 → 0.10.3-pr-480.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.
@@ -4,9 +4,9 @@
4
4
 
5
5
  <script module lang="ts">
6
6
  import type { ChannelAccessor } from '../types/index.js';
7
- import type { RegressionMarkProps as BaseRegressionMarkProps } from './helpers/Regression.svelte';
7
+ import type { RegressionOptions } from './helpers/Regression.svelte';
8
8
 
9
- export type RegressionXMarkProps = BaseRegressionMarkProps & {
9
+ export type RegressionXMarkProps = RegressionOptions & {
10
10
  data?: Record<string | symbol, any>[];
11
11
  z?: ChannelAccessor;
12
12
  };
@@ -1,6 +1,6 @@
1
1
  import type { ChannelAccessor } from '../types/index.js';
2
- import type { RegressionMarkProps as BaseRegressionMarkProps } from './helpers/Regression.svelte';
3
- export type RegressionXMarkProps = BaseRegressionMarkProps & {
2
+ import type { RegressionOptions } from './helpers/Regression.svelte';
3
+ export type RegressionXMarkProps = RegressionOptions & {
4
4
  data?: Record<string | symbol, any>[];
5
5
  z?: ChannelAccessor;
6
6
  };
@@ -3,9 +3,9 @@
3
3
  -->
4
4
  <script module lang="ts">
5
5
  import type { ChannelAccessor } from '../types/index.js';
6
- import type { RegressionMarkProps as BaseRegressionMarkProps } from './helpers/Regression.svelte';
6
+ import type { RegressionOptions } from './helpers/Regression.svelte';
7
7
 
8
- export type RegressionYMarkProps = BaseRegressionMarkProps & {
8
+ export type RegressionYMarkProps = RegressionOptions & {
9
9
  data?: Record<string | symbol, any>[];
10
10
  z?: ChannelAccessor;
11
11
  };
@@ -1,6 +1,6 @@
1
1
  import type { ChannelAccessor } from '../types/index.js';
2
- import type { RegressionMarkProps as BaseRegressionMarkProps } from './helpers/Regression.svelte';
3
- export type RegressionYMarkProps = BaseRegressionMarkProps & {
2
+ import type { RegressionOptions } from './helpers/Regression.svelte';
3
+ export type RegressionYMarkProps = RegressionOptions & {
4
4
  data?: Record<string | symbol, any>[];
5
5
  z?: ChannelAccessor;
6
6
  };
@@ -1,15 +1,15 @@
1
1
  <script module lang="ts">
2
- import type { BaseMarkProps, ChannelAccessor, FacetContext } from '../../types/index.js';
2
+ import type { BaseMarkProps, ChannelAccessor } from '../../types/index.js';
3
3
 
4
- type RegressionType = 'linear' | 'quad' | 'poly' | 'exp' | 'log' | 'pow' | 'loess';
4
+ export type RegressionType = 'linear' | 'quad' | 'poly' | 'exp' | 'log' | 'pow' | 'loess';
5
5
 
6
- export type RegressionMarkProps = BaseMarkProps & {
6
+ export type RegressionOptions = BaseMarkProps & {
7
7
  /** the horizontal position channel; bound to the x scale */
8
8
  x: ChannelAccessor;
9
9
  /** the vertical position channel; bound to the y scale */
10
10
  y: ChannelAccessor;
11
11
  /** the regression model type */
12
- type: RegressionType;
12
+ type?: RegressionType;
13
13
  /**
14
14
  * If order is specified, sets the regression's order to the specified number.
15
15
  * For example, if order is set to 4, the regression generator will perform a
@@ -19,13 +19,13 @@
19
19
  * regression line will fit your data with a high determination coefficient,
20
20
  * it may have little predictive power for data outside of your domain.
21
21
  */
22
- order: number;
22
+ order?: number;
23
23
  /** the base for logarithmic regression */
24
- base: number;
24
+ base?: number;
25
25
  /** the bandwidth for LOESS regression, as a fraction of the data range (0 to 1) */
26
- span: number;
26
+ span?: number;
27
27
  /** the confidence level for confidence bands (e.g. 0.95 for 95% confidence) */
28
- confidence: number;
28
+ confidence?: number | false;
29
29
  };
30
30
  </script>
31
31
 
@@ -45,25 +45,63 @@
45
45
  import { resolveChannel } from '../../helpers/resolve.js';
46
46
  import { confidenceInterval } from '../../helpers/math.js';
47
47
  import callWithProps from '../../helpers/callWithProps.js';
48
- import { isDate } from '../../helpers/typeChecks.js';
49
-
50
- const regressions = new Map<RegressionType, typeof regressionLinear>([
51
- ['linear', regressionLinear],
52
- ['quad', regressionQuad],
53
- ['poly', regressionPoly],
54
- ['exp', regressionExp],
55
- ['log', regressionLog],
56
- ['pow', regressionPow],
57
- ['loess', regressionLoess]
58
- ]);
59
-
60
- function maybeRegression(name: string) {
61
- name = `${name}`.toLowerCase();
62
- if (regressions.has(name)) return regressions.get(name);
63
- throw new Error('unknown regression ' + name);
48
+ import type { DataRecord, FacetContext, RawValue } from '../../types/index.js';
49
+ import { usePlot } from '../../hooks/usePlot.svelte.js';
50
+
51
+ type RegressionData = { x: number; y: number };
52
+ type RegressionOutput = [number, number][] & {
53
+ predict?: (x: number) => number;
54
+ predictMany?: (points: number[]) => number[];
55
+ };
56
+ type RegressionGenerator = ((data: RegressionData[]) => RegressionOutput) & {
57
+ x: (fn: (d: RegressionData) => number) => RegressionGenerator;
58
+ y: (fn: (d: RegressionData) => number) => RegressionGenerator;
59
+ domain?: (domain: [number, number]) => RegressionGenerator;
60
+ order?: (order: number) => RegressionGenerator;
61
+ base?: (base: number) => RegressionGenerator;
62
+ bandwidth?: (span: number) => RegressionGenerator;
63
+ };
64
+ type RegressionFactory = () => RegressionGenerator;
65
+
66
+ interface RegressionProps extends RegressionOptions {
67
+ data: DataRecord[];
68
+ dependent: 'x' | 'y';
64
69
  }
65
70
 
66
- import { usePlot } from '../../hooks/usePlot.svelte.js';
71
+ // Normalize all regression constructors behind one callable adapter shape.
72
+ const regressions: Record<RegressionType, RegressionFactory> = {
73
+ linear: regressionLinear as RegressionFactory,
74
+ quad: regressionQuad as RegressionFactory,
75
+ poly: regressionPoly as RegressionFactory,
76
+ exp: regressionExp as RegressionFactory,
77
+ log: regressionLog as RegressionFactory,
78
+ pow: regressionPow as RegressionFactory,
79
+ loess: regressionLoess as RegressionFactory
80
+ };
81
+
82
+ function maybeRegression(name: RegressionType): RegressionFactory {
83
+ const fn = regressions[name];
84
+ if (fn) return fn;
85
+ throw new Error(`unknown regression ${name}`);
86
+ }
87
+
88
+ // Regression engines operate on numeric domains; convert Date channels to epoch ms.
89
+ function toNumeric(value: RawValue): number {
90
+ return value instanceof Date ? value.valueOf() : Number(value);
91
+ }
92
+
93
+ // Convert generated points back to Date for time scales so downstream marks render correctly.
94
+ function toOutputX(value: number, scaleType: string): RawValue {
95
+ return scaleType === 'time' ? new Date(value) : value;
96
+ }
97
+
98
+ function makeTicks(domain: [number, number], count = 40): number[] {
99
+ const [start, end] = domain;
100
+ if (start === end) return [start];
101
+ const tickCount = Math.max(1, count);
102
+ const step = (end - start) / tickCount;
103
+ return Array.from({ length: tickCount + 1 }, (_, i) => start + i * step);
104
+ }
67
105
 
68
106
  const plot = usePlot();
69
107
 
@@ -72,85 +110,129 @@
72
110
  dependent,
73
111
  type = 'linear',
74
112
  order = 3,
75
- base = 2.71828,
113
+ base = Math.E,
76
114
  span = 0.3,
77
115
  confidence = 0.99,
78
- class: className = null,
116
+ class: className = '',
79
117
  ...options
80
- }: RegressionMarkProps & { dependent: 'x' | 'y' } = $props();
118
+ }: RegressionProps = $props();
81
119
 
82
120
  const { getTestFacet } = getContext<FacetContext>('svelteplot/facet');
83
- let testFacet = $derived(getTestFacet());
121
+ const testFacet = $derived(getTestFacet());
122
+
123
+ const filteredData = $derived(data.filter((d) => testFacet(d, options as any)));
84
124
 
85
- let filteredData = $derived(data.filter((d) => testFacet(d, options)));
125
+ const independent: 'x' | 'y' = $derived(dependent === 'x' ? 'y' : 'x');
86
126
 
87
- let independent: 'x' | 'y' = $derived(dependent === 'x' ? 'y' : 'x');
127
+ const regressionFn = $derived(maybeRegression(type));
88
128
 
89
- let regressionFn = $derived(maybeRegression(type));
129
+ // Build a clean numeric input set for regression fitting, dropping invalid rows early.
130
+ const regressionInput = $derived(
131
+ filteredData
132
+ .map((d) => ({
133
+ x: toNumeric(resolveChannel(independent, d, options as any)),
134
+ y: toNumeric(resolveChannel(dependent, d, options as any))
135
+ }))
136
+ .filter(({ x, y }) => Number.isFinite(x) && Number.isFinite(y))
137
+ );
90
138
 
91
- let regression = $derived(
139
+ const independentDomain = $derived.by(() => {
140
+ // Prefer the active scale domain, but fall back to observed data when the scale
141
+ // is still initializing (common in tests and first render passes).
142
+ const scaleDomain = plot.scales[independent].domain;
143
+ const scaleStart = toNumeric(scaleDomain[0]);
144
+ const scaleEnd = toNumeric(scaleDomain.at(-1) ?? scaleDomain[0]);
145
+ if (Number.isFinite(scaleStart) && Number.isFinite(scaleEnd)) {
146
+ return scaleStart <= scaleEnd
147
+ ? ([scaleStart, scaleEnd] as [number, number])
148
+ : ([scaleEnd, scaleStart] as [number, number]);
149
+ }
150
+ if (regressionInput.length === 0) return null;
151
+ let min = Infinity;
152
+ let max = -Infinity;
153
+ for (const point of regressionInput) {
154
+ if (point.x < min) min = point.x;
155
+ if (point.x > max) max = point.x;
156
+ }
157
+ if (!Number.isFinite(min) || !Number.isFinite(max)) return null;
158
+ return min <= max ? ([min, max] as [number, number]) : ([max, min] as [number, number]);
159
+ });
160
+
161
+ const regression = $derived(
92
162
  callWithProps(regressionFn, [], {
93
- x: (d) => resolveChannel(independent, d, options),
94
- y: (d) => resolveChannel(dependent, d, options),
163
+ x: (d: RegressionData) => d.x,
164
+ y: (d: RegressionData) => d.y,
95
165
  ...(type === 'poly' ? { order } : {}),
96
166
  ...(type === 'log' ? { base } : {}),
97
- ...(!type.startsWith('loess') ? { domain: plot.scales[independent].domain } : {}),
167
+ ...(type !== 'loess' && independentDomain != null ? { domain: independentDomain } : {}),
98
168
  ...(type === 'loess' ? { bandwidth: span } : {})
99
- })(filteredData)
169
+ })(regressionInput)
100
170
  );
101
171
 
102
- let regrPoints = $derived([
103
- ...new Set([
104
- plot.scales[independent].domain[0],
105
- ...plot.scales[independent].fn.ticks(40),
106
- plot.scales[independent].domain[1]
107
- ])
108
- ]);
109
-
110
- let regrData = $derived(
111
- regression.predictMany
112
- ? regression.predictMany(regrPoints).map((__y, i) => ({ __x: regrPoints[i], __y }))
113
- : regression.predict
114
- ? regrPoints.map((__x) => {
115
- // const __x = x;
116
- const __y = regression.predict(__x);
117
- return { __x, __y };
118
- })
119
- : regression.map(([__x, __y]) => ({
120
- __x: plot.scales[independent].type === 'time' ? new Date(__x) : __x,
121
- __y
122
- }))
123
- );
172
+ const regrPoints = $derived.by(() => {
173
+ if (independentDomain == null) return [] as number[];
174
+ // Use scale ticks when available; otherwise synthesize evenly spaced samples.
175
+ const ticks = plot.scales[independent].fn
176
+ .ticks(40)
177
+ .map(toNumeric)
178
+ .filter((value): value is number => Number.isFinite(value));
179
+ const points = ticks.length > 0 ? ticks : makeTicks(independentDomain, 40);
180
+ return [
181
+ ...new Set(
182
+ [independentDomain[0], ...points, independentDomain[1]].filter(
183
+ (value): value is number => Number.isFinite(value)
184
+ )
185
+ )
186
+ ].sort((a, b) => a - b);
187
+ });
124
188
 
125
- let stroke = $derived(
126
- options.stroke != null ? resolveChannel('stroke', filteredData[0], options) : null
127
- );
189
+ const regrData = $derived.by(() => {
190
+ // Prefer batch prediction when supported, then per-point predict, then raw curve output.
191
+ if (typeof regression.predictMany === 'function') {
192
+ return regression.predictMany(regrPoints).map((__y, i) => ({
193
+ __x: toOutputX(regrPoints[i], plot.scales[independent].type),
194
+ __y
195
+ }));
196
+ }
197
+ if (typeof regression.predict === 'function') {
198
+ return regrPoints.map((point) => ({
199
+ __x: toOutputX(point, plot.scales[independent].type),
200
+ __y: regression.predict!(point)
201
+ }));
202
+ }
203
+ return regression.map(([__x, __y]) => ({
204
+ __x: toOutputX(__x, plot.scales[independent].type),
205
+ __y
206
+ }));
207
+ });
128
208
 
129
- let confBandGen = $derived(
130
- confidence !== false && regression.predict
131
- ? confidenceInterval(
132
- data
133
- .map((d) => ({
134
- x: resolveChannel(independent, d, options),
135
- y: resolveChannel(dependent, d, options)
136
- }))
137
- .filter(
138
- ({ x, y }) => (Number.isFinite(x) || isDate(x)) && Number.isFinite(y)
139
- ),
140
- regression.predict,
141
- 1 - confidence
142
- )
209
+ const stroke = $derived(
210
+ options.stroke != null && filteredData.length
211
+ ? resolveChannel('stroke', filteredData[0], options as any)
143
212
  : null
144
213
  );
145
214
 
146
- let confBandData = $derived(
147
- confidence !== false && regression.predict
148
- ? regrPoints.map((x) => {
149
- const { x: __x, left, right } = confBandGen(x);
150
- return { __x, __y1: left, __y2: right };
151
- })
152
- : []
215
+ const confBandGen = $derived(
216
+ // Confidence bands require a predictor function and at least 3 points.
217
+ confidence !== false &&
218
+ typeof confidence === 'number' &&
219
+ typeof regression.predict === 'function' &&
220
+ regressionInput.length > 2
221
+ ? confidenceInterval(regressionInput, regression.predict, 1 - confidence)
222
+ : null
153
223
  );
224
+
225
+ const confBandData = $derived.by(() => {
226
+ if (confBandGen == null) return [];
227
+ return regrPoints.map((x) => {
228
+ const { x: __x, left, right } = confBandGen(x);
229
+ return {
230
+ __x: toOutputX(__x, plot.scales[independent].type),
231
+ __y1: left,
232
+ __y2: right
233
+ };
234
+ });
235
+ });
154
236
  </script>
155
237
 
156
238
  {#if filteredData.length}
@@ -1,12 +1,12 @@
1
1
  import type { BaseMarkProps, ChannelAccessor } from '../../types/index.js';
2
- type RegressionType = 'linear' | 'quad' | 'poly' | 'exp' | 'log' | 'pow' | 'loess';
3
- export type RegressionMarkProps = BaseMarkProps & {
2
+ export type RegressionType = 'linear' | 'quad' | 'poly' | 'exp' | 'log' | 'pow' | 'loess';
3
+ export type RegressionOptions = BaseMarkProps & {
4
4
  /** the horizontal position channel; bound to the x scale */
5
5
  x: ChannelAccessor;
6
6
  /** the vertical position channel; bound to the y scale */
7
7
  y: ChannelAccessor;
8
8
  /** the regression model type */
9
- type: RegressionType;
9
+ type?: RegressionType;
10
10
  /**
11
11
  * If order is specified, sets the regression's order to the specified number.
12
12
  * For example, if order is set to 4, the regression generator will perform a
@@ -16,17 +16,19 @@ export type RegressionMarkProps = BaseMarkProps & {
16
16
  * regression line will fit your data with a high determination coefficient,
17
17
  * it may have little predictive power for data outside of your domain.
18
18
  */
19
- order: number;
19
+ order?: number;
20
20
  /** the base for logarithmic regression */
21
- base: number;
21
+ base?: number;
22
22
  /** the bandwidth for LOESS regression, as a fraction of the data range (0 to 1) */
23
- span: number;
23
+ span?: number;
24
24
  /** the confidence level for confidence bands (e.g. 0.95 for 95% confidence) */
25
- confidence: number;
25
+ confidence?: number | false;
26
26
  };
27
- type $$ComponentProps = RegressionMarkProps & {
27
+ import type { DataRecord } from '../../types/index.js';
28
+ interface RegressionProps extends RegressionOptions {
29
+ data: DataRecord[];
28
30
  dependent: 'x' | 'y';
29
- };
30
- declare const Regression: import("svelte").Component<$$ComponentProps, {}, "">;
31
+ }
32
+ declare const Regression: import("svelte").Component<RegressionProps, {}, "">;
31
33
  type Regression = ReturnType<typeof Regression>;
32
34
  export default Regression;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelteplot",
3
- "version": "0.10.3-pr-479.0",
3
+ "version": "0.10.3-pr-480.0",
4
4
  "description": "A Svelte-native data visualization framework based on the layered grammar of graphics principles.",
5
5
  "keywords": [
6
6
  "svelte",