svelteplot 0.11.1 → 0.12.0-pr-523.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/core/Plot.svelte +31 -1
- package/dist/helpers/autoScales.js +3 -1
- package/dist/helpers/facets.d.ts +2 -2
- package/dist/helpers/rasterInterpolate.d.ts +26 -0
- package/dist/helpers/rasterInterpolate.js +220 -0
- package/dist/marks/Contour.svelte +516 -0
- package/dist/marks/Contour.svelte.d.ts +138 -0
- package/dist/marks/Geo.svelte +9 -5
- package/dist/marks/Line.svelte +52 -15
- package/dist/marks/Raster.svelte +421 -0
- package/dist/marks/Raster.svelte.d.ts +95 -0
- package/dist/marks/Text.svelte +8 -6
- package/dist/marks/helpers/GroupMultiple.svelte +6 -1
- package/dist/marks/helpers/GroupMultiple.svelte.d.ts +1 -0
- package/dist/marks/index.d.ts +2 -0
- package/dist/marks/index.js +2 -0
- package/dist/types/mark.d.ts +1 -1
- package/dist/types/plot.d.ts +10 -1
- package/package.json +5 -1
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
<!-- @component
|
|
2
|
+
Renders contour lines (or filled contour bands) from a scalar field using
|
|
3
|
+
the marching-squares algorithm.
|
|
4
|
+
|
|
5
|
+
Supports the same three input modes as the `Raster` mark:
|
|
6
|
+
|
|
7
|
+
**Dense grid mode** (`data` is a flat row-major array, `width`/`height` are
|
|
8
|
+
set, no `x`/`y` channels): each datum is its own scalar value (unless `value`
|
|
9
|
+
is specified).
|
|
10
|
+
|
|
11
|
+
**Function sampling mode** (`data` is omitted/null, `value` is an
|
|
12
|
+
`(x, y) => number` function): the function is evaluated on a pixel grid.
|
|
13
|
+
|
|
14
|
+
**Scatter interpolation mode** (`data` is an array with `x`/`y` channels):
|
|
15
|
+
each datum contributes a position and scalar value; the mark spatially
|
|
16
|
+
interpolates over the grid before running marching squares.
|
|
17
|
+
|
|
18
|
+
Styling: `fill` and `stroke` accept ordinary CSS color strings **or** the
|
|
19
|
+
special keyword `"value"`, which maps each contour level's threshold through
|
|
20
|
+
the plot's color scale. Defaults: `fill="none"`, `stroke="currentColor"`.
|
|
21
|
+
-->
|
|
22
|
+
<script lang="ts" generics="Datum extends DataRow">
|
|
23
|
+
interface ContourMarkProps {
|
|
24
|
+
/**
|
|
25
|
+
* Input data. For **dense grid** mode supply a flat row-major array and
|
|
26
|
+
* set `width`/`height`. Omit (or set null) for **function-sampling**
|
|
27
|
+
* mode. For **scatter interpolation** supply an array of records with
|
|
28
|
+
* `x`/`y` channels.
|
|
29
|
+
*/
|
|
30
|
+
data?: Datum[] | null;
|
|
31
|
+
/** x position channel (scatter / dense-grid mode) */
|
|
32
|
+
x?: ChannelAccessor<Datum>;
|
|
33
|
+
/** y position channel (scatter / dense-grid mode) */
|
|
34
|
+
y?: ChannelAccessor<Datum>;
|
|
35
|
+
/**
|
|
36
|
+
* Scalar field accessor, identity function for dense grid, or an
|
|
37
|
+
* `(x, y) => number` function for function-sampling mode.
|
|
38
|
+
*/
|
|
39
|
+
value?: ChannelAccessor<Datum> | ((x: number, y: number) => number);
|
|
40
|
+
/**
|
|
41
|
+
* Contour threshold levels. Can be:
|
|
42
|
+
* - a **count** (number): approximately that many nicely-spaced levels
|
|
43
|
+
* - an explicit **array** of threshold values
|
|
44
|
+
* - a **function** `(values, min, max) => number[]`
|
|
45
|
+
* - a d3 **threshold scheme** object with `.floor()` / `.range()`
|
|
46
|
+
*
|
|
47
|
+
* Defaults to Sturges' formula applied to the value range.
|
|
48
|
+
*/
|
|
49
|
+
thresholds?:
|
|
50
|
+
| number
|
|
51
|
+
| number[]
|
|
52
|
+
| ((values: number[], min: number, max: number) => number[])
|
|
53
|
+
| { floor(x: number): number; range(a: number, b: number): number[] };
|
|
54
|
+
/**
|
|
55
|
+
* Step interval between contour levels (alternative to `thresholds`).
|
|
56
|
+
* Can be a number (constant step) or an interval object with `.floor()`
|
|
57
|
+
* / `.range()`.
|
|
58
|
+
*/
|
|
59
|
+
interval?: number | { floor(x: number): number; range(a: number, b: number): number[] };
|
|
60
|
+
/**
|
|
61
|
+
* Whether to apply linear interpolation when tracing contour edges
|
|
62
|
+
* (default `true`). Set to `false` for a blockier, faster appearance.
|
|
63
|
+
*/
|
|
64
|
+
smooth?: boolean;
|
|
65
|
+
/** left bound of the domain in data coordinates */
|
|
66
|
+
x1?: number;
|
|
67
|
+
/** top bound of the domain in data coordinates */
|
|
68
|
+
y1?: number;
|
|
69
|
+
/** right bound of the domain in data coordinates */
|
|
70
|
+
x2?: number;
|
|
71
|
+
/** bottom bound of the domain in data coordinates */
|
|
72
|
+
y2?: number;
|
|
73
|
+
/**
|
|
74
|
+
* Explicit grid width; required for dense grid mode, overrides
|
|
75
|
+
* `pixelSize` in other modes.
|
|
76
|
+
*/
|
|
77
|
+
width?: number;
|
|
78
|
+
/**
|
|
79
|
+
* Explicit grid height; required for dense grid mode, overrides
|
|
80
|
+
* `pixelSize` in other modes.
|
|
81
|
+
*/
|
|
82
|
+
height?: number;
|
|
83
|
+
/** pixel size in screen pixels (default 2) */
|
|
84
|
+
pixelSize?: number;
|
|
85
|
+
/** Gaussian blur radius applied before contouring (default 0) */
|
|
86
|
+
blur?: number;
|
|
87
|
+
/**
|
|
88
|
+
* Spatial interpolation for scatter mode:
|
|
89
|
+
* `"none"` | `"nearest"` | `"barycentric"` | `"random-walk"` or a
|
|
90
|
+
* custom `(index, w, h, X, Y, V) => W` function.
|
|
91
|
+
* Defaults to `"nearest"` when data is provided.
|
|
92
|
+
*/
|
|
93
|
+
interpolate?: 'none' | 'nearest' | 'barycentric' | 'random-walk' | InterpolateFunction;
|
|
94
|
+
/**
|
|
95
|
+
* Fill color for contour polygons. Use `"value"` to map each
|
|
96
|
+
* threshold level through the plot's color scale. Default `"none"`.
|
|
97
|
+
*/
|
|
98
|
+
fill?: string;
|
|
99
|
+
/**
|
|
100
|
+
* Stroke color for contour lines. Use `"value"` to map each
|
|
101
|
+
* threshold level through the plot's color scale. Default
|
|
102
|
+
* `"currentColor"`.
|
|
103
|
+
*/
|
|
104
|
+
stroke?: string;
|
|
105
|
+
strokeWidth?: number;
|
|
106
|
+
strokeOpacity?: number;
|
|
107
|
+
fillOpacity?: number;
|
|
108
|
+
opacity?: number;
|
|
109
|
+
strokeMiterlimit?: number;
|
|
110
|
+
clipPath?: string;
|
|
111
|
+
class?: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
import type {
|
|
115
|
+
DataRow,
|
|
116
|
+
DataRecord,
|
|
117
|
+
ChannelAccessor,
|
|
118
|
+
ScaledDataRecord,
|
|
119
|
+
MarkType
|
|
120
|
+
} from '../types/index.js';
|
|
121
|
+
import { blur2, ticks, nice, thresholdSturges } from 'd3-array';
|
|
122
|
+
import { contours } from 'd3-contour';
|
|
123
|
+
import { geoPath } from 'd3-geo';
|
|
124
|
+
import Mark from '../Mark.svelte';
|
|
125
|
+
import { usePlot } from '../hooks/usePlot.svelte.js';
|
|
126
|
+
import {
|
|
127
|
+
interpolateNone,
|
|
128
|
+
interpolateNearest,
|
|
129
|
+
interpolatorBarycentric,
|
|
130
|
+
interpolatorRandomWalk,
|
|
131
|
+
type InterpolateFunction
|
|
132
|
+
} from '../helpers/rasterInterpolate.js';
|
|
133
|
+
import { X, Y, RAW_VALUE } from '../transforms/recordize.js';
|
|
134
|
+
import { scaleLinear } from 'd3-scale';
|
|
135
|
+
|
|
136
|
+
let markProps: ContourMarkProps = $props();
|
|
137
|
+
|
|
138
|
+
const {
|
|
139
|
+
data,
|
|
140
|
+
value,
|
|
141
|
+
x1: x1Prop,
|
|
142
|
+
y1: y1Prop,
|
|
143
|
+
x2: x2Prop,
|
|
144
|
+
y2: y2Prop,
|
|
145
|
+
width: widthProp,
|
|
146
|
+
height: heightProp,
|
|
147
|
+
pixelSize = 4,
|
|
148
|
+
blur = 0,
|
|
149
|
+
smooth = true,
|
|
150
|
+
thresholds,
|
|
151
|
+
interval,
|
|
152
|
+
interpolate,
|
|
153
|
+
fill = 'none',
|
|
154
|
+
stroke,
|
|
155
|
+
strokeWidth,
|
|
156
|
+
strokeOpacity,
|
|
157
|
+
fillOpacity,
|
|
158
|
+
opacity,
|
|
159
|
+
strokeMiterlimit = 1,
|
|
160
|
+
clipPath,
|
|
161
|
+
class: className = '',
|
|
162
|
+
...options
|
|
163
|
+
}: ContourMarkProps = $derived({ ...markProps });
|
|
164
|
+
|
|
165
|
+
const plot = usePlot();
|
|
166
|
+
|
|
167
|
+
/** No data: value is an (x,y) function */
|
|
168
|
+
const isSamplerMode = $derived(data == null);
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Dense grid: data is a flat array, width+height are given, no x/y channels.
|
|
172
|
+
*/
|
|
173
|
+
const isDenseGridMode = $derived(
|
|
174
|
+
data != null &&
|
|
175
|
+
widthProp != null &&
|
|
176
|
+
heightProp != null &&
|
|
177
|
+
(options as any).x == null &&
|
|
178
|
+
(options as any).y == null
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const interpolateFn = $derived(resolveInterpolate(interpolate));
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Coarse 20×20 sample of the value function in data-space.
|
|
185
|
+
* Used only to give `<Mark>` representative values for color-scale
|
|
186
|
+
* domain/type detection. The full-resolution grid is (re-)computed on
|
|
187
|
+
* every render inside `computeContours`, so impure functions (closures
|
|
188
|
+
* over reactive state, `performance.now()`, etc.) always reflect the
|
|
189
|
+
* current render state.
|
|
190
|
+
*/
|
|
191
|
+
const samplerSampleValues = $derived.by((): number[] | null => {
|
|
192
|
+
if (!isSamplerMode || typeof value !== 'function') return null;
|
|
193
|
+
if (x1Prop == null || x2Prop == null || y1Prop == null || y2Prop == null) return null;
|
|
194
|
+
const SAMPLES = 20;
|
|
195
|
+
const dx = x2Prop - x1Prop;
|
|
196
|
+
const dy = y2Prop - y1Prop;
|
|
197
|
+
const fn = value as (x: number, y: number) => number;
|
|
198
|
+
const out: number[] = [];
|
|
199
|
+
for (let yi = 0; yi < SAMPLES; yi++) {
|
|
200
|
+
for (let xi = 0; xi < SAMPLES; xi++) {
|
|
201
|
+
const v = fn(
|
|
202
|
+
x1Prop + ((xi + 0.5) / SAMPLES) * dx,
|
|
203
|
+
y1Prop + ((yi + 0.5) / SAMPLES) * dy
|
|
204
|
+
);
|
|
205
|
+
if (isFinite(v)) out.push(v);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return out.length > 0 ? out : null;
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
function resolveInterpolate(interp: ContourMarkProps['interpolate']): InterpolateFunction {
|
|
212
|
+
if (typeof interp === 'function') return interp;
|
|
213
|
+
// Default to nearest for scatter mode
|
|
214
|
+
const resolved = interp ?? (isSamplerMode || isDenseGridMode ? 'none' : 'nearest');
|
|
215
|
+
switch (String(resolved).toLowerCase()) {
|
|
216
|
+
case 'none':
|
|
217
|
+
return interpolateNone;
|
|
218
|
+
case 'nearest':
|
|
219
|
+
return interpolateNearest;
|
|
220
|
+
case 'barycentric':
|
|
221
|
+
return interpolatorBarycentric();
|
|
222
|
+
case 'random-walk':
|
|
223
|
+
return interpolatorRandomWalk();
|
|
224
|
+
}
|
|
225
|
+
throw new Error(`invalid interpolate: ${interp}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Pixel-space bounds of the plot area. */
|
|
229
|
+
function getBounds() {
|
|
230
|
+
const facetWidth = plot.facetWidth ?? 100;
|
|
231
|
+
const facetHeight = plot.facetHeight ?? 100;
|
|
232
|
+
const marginLeft = plot.options.marginLeft ?? 0;
|
|
233
|
+
const marginTop = plot.options.marginTop ?? 0;
|
|
234
|
+
return {
|
|
235
|
+
bx1: marginLeft,
|
|
236
|
+
by1: marginTop,
|
|
237
|
+
bx2: marginLeft + facetWidth,
|
|
238
|
+
by2: marginTop + facetHeight
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Resolve a value accessor on a single datum. */
|
|
243
|
+
function resolveValue(datum: any): number | null {
|
|
244
|
+
if (value == null) return typeof datum === 'number' ? datum : null;
|
|
245
|
+
if (typeof value === 'string') return datum[value] ?? null;
|
|
246
|
+
if (typeof value === 'function') return (value as (d: any) => number)(datum);
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
type ContourGeometry = {
|
|
251
|
+
type: 'MultiPolygon';
|
|
252
|
+
coordinates: number[][][][];
|
|
253
|
+
value: number;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Compute the scalar-field grid and run marching squares.
|
|
258
|
+
* Returns an array of GeoJSON MultiPolygon objects in SVG pixel coordinates,
|
|
259
|
+
* each carrying the threshold `value` that produced it.
|
|
260
|
+
*/
|
|
261
|
+
function computeContours(scaledData: ScaledDataRecord[]): ContourGeometry[] | null {
|
|
262
|
+
const { bx1, by1, bx2, by2 } = getBounds();
|
|
263
|
+
const dx = bx2 - bx1;
|
|
264
|
+
const dy = by2 - by1;
|
|
265
|
+
const w = widthProp ?? Math.round(Math.abs(dx) / pixelSize);
|
|
266
|
+
const h = heightProp ?? Math.round(Math.abs(dy) / pixelSize);
|
|
267
|
+
if (w <= 0 || h <= 0) return null;
|
|
268
|
+
const n = w * h;
|
|
269
|
+
|
|
270
|
+
// --- Build numeric value grid V ---
|
|
271
|
+
let V: number[] | null = null;
|
|
272
|
+
|
|
273
|
+
if (isDenseGridMode) {
|
|
274
|
+
// Dense grid: each datum is a value; flip rows (y-up → screen y-down)
|
|
275
|
+
const Vraw = (data as any[]).map((d) => resolveValue(d) ?? NaN);
|
|
276
|
+
V = new Array(n);
|
|
277
|
+
for (let row = 0; row < h; ++row) {
|
|
278
|
+
const srcRow = h - 1 - row;
|
|
279
|
+
for (let col = 0; col < w; ++col) {
|
|
280
|
+
V[row * w + col] = Vraw[srcRow * w + col];
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
} else if (isSamplerMode) {
|
|
284
|
+
// Sampler mode: evaluate (x, y) => number on the full pixel grid
|
|
285
|
+
if (typeof value !== 'function') return null;
|
|
286
|
+
const xScale = scaleLinear().range([x1Prop!, x2Prop!]).domain([bx1, bx2]);
|
|
287
|
+
const yScale = scaleLinear().range([y1Prop!, y2Prop!]).domain([by1, by2]);
|
|
288
|
+
const kx = dx / w;
|
|
289
|
+
const ky = dy / h;
|
|
290
|
+
V = new Array(n);
|
|
291
|
+
let i = 0;
|
|
292
|
+
for (let yi = 0.5; yi < h; ++yi) {
|
|
293
|
+
for (let xi = 0.5; xi < w; ++xi, ++i) {
|
|
294
|
+
V[i] = (value as (x: number, y: number) => number)(
|
|
295
|
+
xScale(bx1 + xi * kx),
|
|
296
|
+
yScale(by1 + yi * ky)
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
} else if (scaledData.length > 0) {
|
|
301
|
+
// Scatter interpolation mode
|
|
302
|
+
const validData = scaledData.filter((d) => d.valid && d.x != null && d.y != null);
|
|
303
|
+
if (validData.length === 0) return null;
|
|
304
|
+
const kx = w / dx;
|
|
305
|
+
const ky = h / dy;
|
|
306
|
+
const index = validData.map((_, i) => i);
|
|
307
|
+
const IX = new Float64Array(validData.map((d) => ((d.x as number) - bx1) * kx));
|
|
308
|
+
const IY = new Float64Array(validData.map((d) => ((d.y as number) - by1) * ky));
|
|
309
|
+
const rawValues = validData.map((d) => d.resolved?.fill);
|
|
310
|
+
if (rawValues.some((v) => v != null)) {
|
|
311
|
+
V = Array.from(interpolateFn(index, w, h, IX, IY, rawValues));
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (!V) return null;
|
|
316
|
+
|
|
317
|
+
// --- Optional Gaussian blur ---
|
|
318
|
+
if (blur > 0) blur2({ data: V, width: w, height: h }, blur);
|
|
319
|
+
|
|
320
|
+
// --- Compute thresholds ---
|
|
321
|
+
const T = computeThresholds(V, w, h, thresholds, interval);
|
|
322
|
+
if (T.length === 0) return null;
|
|
323
|
+
|
|
324
|
+
// --- Run marching squares ---
|
|
325
|
+
const kx = w / dx;
|
|
326
|
+
const ky = h / dy;
|
|
327
|
+
const contourFn = contours().size([w, h]).smooth(smooth);
|
|
328
|
+
|
|
329
|
+
const contourData: ContourGeometry[] = T.map((t) => {
|
|
330
|
+
const geom = contourFn.contour(V!, t) as ContourGeometry;
|
|
331
|
+
|
|
332
|
+
// Rescale from grid coordinates to SVG pixel coordinates
|
|
333
|
+
for (const rings of geom.coordinates) {
|
|
334
|
+
for (const ring of rings) {
|
|
335
|
+
for (const point of ring) {
|
|
336
|
+
point[0] = point[0] / kx + bx1;
|
|
337
|
+
point[1] = point[1] / ky + by1;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return geom;
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
return contourData;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Compute an array of threshold tick values from the V grid. */
|
|
349
|
+
function computeThresholds(
|
|
350
|
+
V: number[],
|
|
351
|
+
w: number,
|
|
352
|
+
h: number,
|
|
353
|
+
thresholdSpec: ContourMarkProps['thresholds'],
|
|
354
|
+
intervalSpec: ContourMarkProps['interval']
|
|
355
|
+
): number[] {
|
|
356
|
+
// Compute finite extent
|
|
357
|
+
let vMin = Infinity,
|
|
358
|
+
vMax = -Infinity;
|
|
359
|
+
for (const v of V) {
|
|
360
|
+
if (isFinite(v)) {
|
|
361
|
+
if (v < vMin) vMin = v;
|
|
362
|
+
if (v > vMax) vMax = v;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (!isFinite(vMin) || !isFinite(vMax) || vMin === vMax) return [];
|
|
366
|
+
|
|
367
|
+
// Resolve interval shorthand into a thresholds-compatible object
|
|
368
|
+
let t: ContourMarkProps['thresholds'] = thresholdSpec;
|
|
369
|
+
if (t == null && intervalSpec != null) {
|
|
370
|
+
if (typeof intervalSpec === 'number') {
|
|
371
|
+
const step = intervalSpec;
|
|
372
|
+
t = {
|
|
373
|
+
floor: (x: number) => Math.floor(x / step) * step,
|
|
374
|
+
range: (a: number, b: number) => {
|
|
375
|
+
const result: number[] = [];
|
|
376
|
+
let v = Math.ceil(a / step) * step;
|
|
377
|
+
while (v < b) {
|
|
378
|
+
result.push(v);
|
|
379
|
+
v += step;
|
|
380
|
+
}
|
|
381
|
+
return result;
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
} else {
|
|
385
|
+
t = intervalSpec;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Default to Sturges' formula (returns a count, handled below)
|
|
390
|
+
const tSpec: any = t ?? thresholdSturges;
|
|
391
|
+
|
|
392
|
+
// Interval object with floor/range
|
|
393
|
+
if (typeof tSpec === 'object' && tSpec !== null && 'range' in tSpec) {
|
|
394
|
+
return tSpec.range(tSpec.floor(vMin), vMax);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Already an array
|
|
398
|
+
if (Array.isArray(tSpec)) return tSpec;
|
|
399
|
+
|
|
400
|
+
// Function form — may return a count (number) or an array of thresholds
|
|
401
|
+
let resolved: any = tSpec;
|
|
402
|
+
if (typeof tSpec === 'function') {
|
|
403
|
+
const finiteV = V.filter(isFinite);
|
|
404
|
+
resolved = tSpec(finiteV, vMin, vMax);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (Array.isArray(resolved)) return resolved;
|
|
408
|
+
|
|
409
|
+
// Count form: approximately `resolved` nicely-spaced levels
|
|
410
|
+
const count = resolved as number;
|
|
411
|
+
const [nMin, nMax] = nice(vMin, vMax, count) as [number, number];
|
|
412
|
+
const tz = ticks(nMin, nMax, count);
|
|
413
|
+
while (tz.length > 0 && tz[tz.length - 1] >= vMax) tz.pop();
|
|
414
|
+
while (tz.length > 1 && tz[1] < vMin) tz.shift();
|
|
415
|
+
return tz;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** Resolve a fill or stroke prop for a given contour level. */
|
|
419
|
+
function resolveColor(prop: string | undefined, threshold: number): string {
|
|
420
|
+
if (prop === 'value') {
|
|
421
|
+
return (plot.scales.color?.fn(threshold) as string) ?? 'currentColor';
|
|
422
|
+
}
|
|
423
|
+
return prop ?? 'none';
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** When a fill is active, stroke defaults to none; otherwise currentColor. */
|
|
427
|
+
const effectiveStroke = $derived(stroke ?? (fill !== 'none' ? 'none' : 'currentColor'));
|
|
428
|
+
|
|
429
|
+
/** Build the inline style string for a single contour path. */
|
|
430
|
+
function contourStyle(threshold: number): string {
|
|
431
|
+
const parts: string[] = [];
|
|
432
|
+
parts.push(`fill:${resolveColor(fill, threshold)}`);
|
|
433
|
+
parts.push(`stroke:${resolveColor(effectiveStroke, threshold)}`);
|
|
434
|
+
if (strokeWidth != null) parts.push(`stroke-width:${strokeWidth}`);
|
|
435
|
+
if (strokeOpacity != null) parts.push(`stroke-opacity:${strokeOpacity}`);
|
|
436
|
+
if (fillOpacity != null) parts.push(`fill-opacity:${fillOpacity}`);
|
|
437
|
+
if (opacity != null) parts.push(`opacity:${opacity}`);
|
|
438
|
+
if (strokeMiterlimit != null) parts.push(`stroke-miterlimit:${strokeMiterlimit}`);
|
|
439
|
+
return parts.join(';');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const path = geoPath();
|
|
443
|
+
|
|
444
|
+
// --- Mark registration data ---
|
|
445
|
+
|
|
446
|
+
/** For dense grid mode: synthetic records with symbol-keyed x/y/value. */
|
|
447
|
+
const denseMarkData = $derived(
|
|
448
|
+
isDenseGridMode
|
|
449
|
+
? (data as any[]).map((d, i) => ({
|
|
450
|
+
[X]: i % widthProp!,
|
|
451
|
+
[Y]: Math.floor(i / widthProp!),
|
|
452
|
+
[RAW_VALUE]: resolveValue(d)
|
|
453
|
+
}))
|
|
454
|
+
: null
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* For sampler mode: corner records to establish x/y scale domains, plus
|
|
459
|
+
* the coarse sample values for color-scale domain registration.
|
|
460
|
+
*/
|
|
461
|
+
const samplerMarkData = $derived.by(() => {
|
|
462
|
+
if (!isSamplerMode) return null;
|
|
463
|
+
const x1 = x1Prop,
|
|
464
|
+
x2 = x2Prop,
|
|
465
|
+
y1 = y1Prop,
|
|
466
|
+
y2 = y2Prop;
|
|
467
|
+
if (x1 == null || x2 == null || y1 == null || y2 == null) return null;
|
|
468
|
+
const samples = samplerSampleValues;
|
|
469
|
+
const records: any[] = [
|
|
470
|
+
{ [X]: x1, [Y]: y1 },
|
|
471
|
+
{ [X]: x2, [Y]: y2 }
|
|
472
|
+
];
|
|
473
|
+
if (samples) {
|
|
474
|
+
for (const v of samples) {
|
|
475
|
+
records.push({ [X]: x1, [Y]: y1, [RAW_VALUE]: v });
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return records as DataRecord[];
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
// Always include 'fill' so the color scale domain is built from scalar field
|
|
482
|
+
// values in all three modes. The actual fill/stroke style is resolved
|
|
483
|
+
// per-path in the template.
|
|
484
|
+
const markChannels = ['x', 'y', 'fill'] as const;
|
|
485
|
+
|
|
486
|
+
const markFill = $derived(
|
|
487
|
+
isDenseGridMode || isSamplerMode ? (RAW_VALUE as any) : (value as any)
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
const markX = $derived(isDenseGridMode || isSamplerMode ? (X as any) : undefined);
|
|
491
|
+
const markY = $derived(isDenseGridMode || isSamplerMode ? (Y as any) : undefined);
|
|
492
|
+
</script>
|
|
493
|
+
|
|
494
|
+
<Mark
|
|
495
|
+
type={'contour' as MarkType}
|
|
496
|
+
data={isDenseGridMode
|
|
497
|
+
? (denseMarkData as DataRecord[])
|
|
498
|
+
: isSamplerMode
|
|
499
|
+
? ((samplerMarkData ?? []) as DataRecord[])
|
|
500
|
+
: ((data ?? []) as DataRecord[])}
|
|
501
|
+
channels={markChannels as any}
|
|
502
|
+
x={markX}
|
|
503
|
+
y={markY}
|
|
504
|
+
fill={markFill}
|
|
505
|
+
{...options}>
|
|
506
|
+
{#snippet children({ scaledData })}
|
|
507
|
+
{@const contourPaths = computeContours(scaledData)}
|
|
508
|
+
{#if contourPaths}
|
|
509
|
+
<g clip-path={clipPath} class={className || null} aria-label="contour">
|
|
510
|
+
{#each contourPaths as contourGeom (contourGeom.value)}
|
|
511
|
+
<path d={path(contourGeom as any)} style={contourStyle(contourGeom.value)} />
|
|
512
|
+
{/each}
|
|
513
|
+
</g>
|
|
514
|
+
{/if}
|
|
515
|
+
{/snippet}
|
|
516
|
+
</Mark>
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { DataRow, ChannelAccessor } from '../types/index.js';
|
|
2
|
+
import { type InterpolateFunction } from '../helpers/rasterInterpolate.js';
|
|
3
|
+
declare function $$render<Datum extends DataRow>(): {
|
|
4
|
+
props: {
|
|
5
|
+
/**
|
|
6
|
+
* Input data. For **dense grid** mode supply a flat row-major array and
|
|
7
|
+
* set `width`/`height`. Omit (or set null) for **function-sampling**
|
|
8
|
+
* mode. For **scatter interpolation** supply an array of records with
|
|
9
|
+
* `x`/`y` channels.
|
|
10
|
+
*/
|
|
11
|
+
data?: Datum[] | null;
|
|
12
|
+
/** x position channel (scatter / dense-grid mode) */
|
|
13
|
+
x?: ChannelAccessor<Datum>;
|
|
14
|
+
/** y position channel (scatter / dense-grid mode) */
|
|
15
|
+
y?: ChannelAccessor<Datum>;
|
|
16
|
+
/**
|
|
17
|
+
* Scalar field accessor, identity function for dense grid, or an
|
|
18
|
+
* `(x, y) => number` function for function-sampling mode.
|
|
19
|
+
*/
|
|
20
|
+
value?: ChannelAccessor<Datum> | ((x: number, y: number) => number);
|
|
21
|
+
/**
|
|
22
|
+
* Contour threshold levels. Can be:
|
|
23
|
+
* - a **count** (number): approximately that many nicely-spaced levels
|
|
24
|
+
* - an explicit **array** of threshold values
|
|
25
|
+
* - a **function** `(values, min, max) => number[]`
|
|
26
|
+
* - a d3 **threshold scheme** object with `.floor()` / `.range()`
|
|
27
|
+
*
|
|
28
|
+
* Defaults to Sturges' formula applied to the value range.
|
|
29
|
+
*/
|
|
30
|
+
thresholds?: number | number[] | ((values: number[], min: number, max: number) => number[]) | {
|
|
31
|
+
floor(x: number): number;
|
|
32
|
+
range(a: number, b: number): number[];
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Step interval between contour levels (alternative to `thresholds`).
|
|
36
|
+
* Can be a number (constant step) or an interval object with `.floor()`
|
|
37
|
+
* / `.range()`.
|
|
38
|
+
*/
|
|
39
|
+
interval?: number | {
|
|
40
|
+
floor(x: number): number;
|
|
41
|
+
range(a: number, b: number): number[];
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Whether to apply linear interpolation when tracing contour edges
|
|
45
|
+
* (default `true`). Set to `false` for a blockier, faster appearance.
|
|
46
|
+
*/
|
|
47
|
+
smooth?: boolean;
|
|
48
|
+
/** left bound of the domain in data coordinates */
|
|
49
|
+
x1?: number;
|
|
50
|
+
/** top bound of the domain in data coordinates */
|
|
51
|
+
y1?: number;
|
|
52
|
+
/** right bound of the domain in data coordinates */
|
|
53
|
+
x2?: number;
|
|
54
|
+
/** bottom bound of the domain in data coordinates */
|
|
55
|
+
y2?: number;
|
|
56
|
+
/**
|
|
57
|
+
* Explicit grid width; required for dense grid mode, overrides
|
|
58
|
+
* `pixelSize` in other modes.
|
|
59
|
+
*/
|
|
60
|
+
width?: number;
|
|
61
|
+
/**
|
|
62
|
+
* Explicit grid height; required for dense grid mode, overrides
|
|
63
|
+
* `pixelSize` in other modes.
|
|
64
|
+
*/
|
|
65
|
+
height?: number;
|
|
66
|
+
/** pixel size in screen pixels (default 2) */
|
|
67
|
+
pixelSize?: number;
|
|
68
|
+
/** Gaussian blur radius applied before contouring (default 0) */
|
|
69
|
+
blur?: number;
|
|
70
|
+
/**
|
|
71
|
+
* Spatial interpolation for scatter mode:
|
|
72
|
+
* `"none"` | `"nearest"` | `"barycentric"` | `"random-walk"` or a
|
|
73
|
+
* custom `(index, w, h, X, Y, V) => W` function.
|
|
74
|
+
* Defaults to `"nearest"` when data is provided.
|
|
75
|
+
*/
|
|
76
|
+
interpolate?: "none" | "nearest" | "barycentric" | "random-walk" | InterpolateFunction;
|
|
77
|
+
/**
|
|
78
|
+
* Fill color for contour polygons. Use `"value"` to map each
|
|
79
|
+
* threshold level through the plot's color scale. Default `"none"`.
|
|
80
|
+
*/
|
|
81
|
+
fill?: string;
|
|
82
|
+
/**
|
|
83
|
+
* Stroke color for contour lines. Use `"value"` to map each
|
|
84
|
+
* threshold level through the plot's color scale. Default
|
|
85
|
+
* `"currentColor"`.
|
|
86
|
+
*/
|
|
87
|
+
stroke?: string;
|
|
88
|
+
strokeWidth?: number;
|
|
89
|
+
strokeOpacity?: number;
|
|
90
|
+
fillOpacity?: number;
|
|
91
|
+
opacity?: number;
|
|
92
|
+
strokeMiterlimit?: number;
|
|
93
|
+
clipPath?: string;
|
|
94
|
+
class?: string;
|
|
95
|
+
};
|
|
96
|
+
exports: {};
|
|
97
|
+
bindings: "";
|
|
98
|
+
slots: {};
|
|
99
|
+
events: {};
|
|
100
|
+
};
|
|
101
|
+
declare class __sveltets_Render<Datum extends DataRow> {
|
|
102
|
+
props(): ReturnType<typeof $$render<Datum>>['props'];
|
|
103
|
+
events(): ReturnType<typeof $$render<Datum>>['events'];
|
|
104
|
+
slots(): ReturnType<typeof $$render<Datum>>['slots'];
|
|
105
|
+
bindings(): "";
|
|
106
|
+
exports(): {};
|
|
107
|
+
}
|
|
108
|
+
interface $$IsomorphicComponent {
|
|
109
|
+
new <Datum extends DataRow>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<Datum>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<Datum>['props']>, ReturnType<__sveltets_Render<Datum>['events']>, ReturnType<__sveltets_Render<Datum>['slots']>> & {
|
|
110
|
+
$$bindings?: ReturnType<__sveltets_Render<Datum>['bindings']>;
|
|
111
|
+
} & ReturnType<__sveltets_Render<Datum>['exports']>;
|
|
112
|
+
<Datum extends DataRow>(internal: unknown, props: ReturnType<__sveltets_Render<Datum>['props']> & {}): ReturnType<__sveltets_Render<Datum>['exports']>;
|
|
113
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Renders contour lines (or filled contour bands) from a scalar field using
|
|
117
|
+
* the marching-squares algorithm.
|
|
118
|
+
*
|
|
119
|
+
* Supports the same three input modes as the `Raster` mark:
|
|
120
|
+
*
|
|
121
|
+
* **Dense grid mode** (`data` is a flat row-major array, `width`/`height` are
|
|
122
|
+
* set, no `x`/`y` channels): each datum is its own scalar value (unless `value`
|
|
123
|
+
* is specified).
|
|
124
|
+
*
|
|
125
|
+
* **Function sampling mode** (`data` is omitted/null, `value` is an
|
|
126
|
+
* `(x, y) => number` function): the function is evaluated on a pixel grid.
|
|
127
|
+
*
|
|
128
|
+
* **Scatter interpolation mode** (`data` is an array with `x`/`y` channels):
|
|
129
|
+
* each datum contributes a position and scalar value; the mark spatially
|
|
130
|
+
* interpolates over the grid before running marching squares.
|
|
131
|
+
*
|
|
132
|
+
* Styling: `fill` and `stroke` accept ordinary CSS color strings **or** the
|
|
133
|
+
* special keyword `"value"`, which maps each contour level's threshold through
|
|
134
|
+
* the plot's color scale. Defaults: `fill="none"`, `stroke="currentColor"`.
|
|
135
|
+
*/
|
|
136
|
+
declare const Contour: $$IsomorphicComponent;
|
|
137
|
+
type Contour<Datum extends DataRow> = InstanceType<typeof Contour<Datum>>;
|
|
138
|
+
export default Contour;
|