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.
- package/dist/marks/RegressionX.svelte +2 -2
- package/dist/marks/RegressionX.svelte.d.ts +2 -2
- package/dist/marks/RegressionY.svelte +2 -2
- package/dist/marks/RegressionY.svelte.d.ts +2 -2
- package/dist/marks/helpers/Regression.svelte +165 -83
- package/dist/marks/helpers/Regression.svelte.d.ts +12 -10
- package/package.json +1 -1
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
<script module lang="ts">
|
|
6
6
|
import type { ChannelAccessor } from '../types/index.js';
|
|
7
|
-
import type {
|
|
7
|
+
import type { RegressionOptions } from './helpers/Regression.svelte';
|
|
8
8
|
|
|
9
|
-
export type RegressionXMarkProps =
|
|
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 {
|
|
3
|
-
export type RegressionXMarkProps =
|
|
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 {
|
|
6
|
+
import type { RegressionOptions } from './helpers/Regression.svelte';
|
|
7
7
|
|
|
8
|
-
export type RegressionYMarkProps =
|
|
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 {
|
|
3
|
-
export type RegressionYMarkProps =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
22
|
+
order?: number;
|
|
23
23
|
/** the base for logarithmic regression */
|
|
24
|
-
base
|
|
24
|
+
base?: number;
|
|
25
25
|
/** the bandwidth for LOESS regression, as a fraction of the data range (0 to 1) */
|
|
26
|
-
span
|
|
26
|
+
span?: number;
|
|
27
27
|
/** the confidence level for confidence bands (e.g. 0.95 for 95% confidence) */
|
|
28
|
-
confidence
|
|
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 {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
[
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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 =
|
|
113
|
+
base = Math.E,
|
|
76
114
|
span = 0.3,
|
|
77
115
|
confidence = 0.99,
|
|
78
|
-
class: className =
|
|
116
|
+
class: className = '',
|
|
79
117
|
...options
|
|
80
|
-
}:
|
|
118
|
+
}: RegressionProps = $props();
|
|
81
119
|
|
|
82
120
|
const { getTestFacet } = getContext<FacetContext>('svelteplot/facet');
|
|
83
|
-
|
|
121
|
+
const testFacet = $derived(getTestFacet());
|
|
122
|
+
|
|
123
|
+
const filteredData = $derived(data.filter((d) => testFacet(d, options as any)));
|
|
84
124
|
|
|
85
|
-
|
|
125
|
+
const independent: 'x' | 'y' = $derived(dependent === 'x' ? 'y' : 'x');
|
|
86
126
|
|
|
87
|
-
|
|
127
|
+
const regressionFn = $derived(maybeRegression(type));
|
|
88
128
|
|
|
89
|
-
|
|
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
|
-
|
|
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) =>
|
|
94
|
-
y: (d) =>
|
|
163
|
+
x: (d: RegressionData) => d.x,
|
|
164
|
+
y: (d: RegressionData) => d.y,
|
|
95
165
|
...(type === 'poly' ? { order } : {}),
|
|
96
166
|
...(type === 'log' ? { base } : {}),
|
|
97
|
-
...(
|
|
167
|
+
...(type !== 'loess' && independentDomain != null ? { domain: independentDomain } : {}),
|
|
98
168
|
...(type === 'loess' ? { bandwidth: span } : {})
|
|
99
|
-
})(
|
|
169
|
+
})(regressionInput)
|
|
100
170
|
);
|
|
101
171
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
126
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
?
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
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
|
|
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
|
|
19
|
+
order?: number;
|
|
20
20
|
/** the base for logarithmic regression */
|
|
21
|
-
base
|
|
21
|
+
base?: number;
|
|
22
22
|
/** the bandwidth for LOESS regression, as a fraction of the data range (0 to 1) */
|
|
23
|
-
span
|
|
23
|
+
span?: number;
|
|
24
24
|
/** the confidence level for confidence bands (e.g. 0.95 for 95% confidence) */
|
|
25
|
-
confidence
|
|
25
|
+
confidence?: number | false;
|
|
26
26
|
};
|
|
27
|
-
type
|
|
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
|
|
31
|
+
}
|
|
32
|
+
declare const Regression: import("svelte").Component<RegressionProps, {}, "">;
|
|
31
33
|
type Regression = ReturnType<typeof Regression>;
|
|
32
34
|
export default Regression;
|