svelteplot 0.11.1 → 0.12.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 +35 -2
- 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/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 +1 -0
- package/dist/marks/index.js +1 -0
- package/dist/types/mark.d.ts +1 -1
- package/dist/types/plot.d.ts +10 -1
- package/package.json +181 -181
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
<!-- @component
|
|
2
|
+
Renders a raster image in one of three modes:
|
|
3
|
+
|
|
4
|
+
**Dense grid mode** (`data` is an array, `width` and `height` are set, no
|
|
5
|
+
`x`/`y` channels): the data is treated as a row-major grid of fill values.
|
|
6
|
+
`fill` defaults to the identity function (each datum is its own value).
|
|
7
|
+
|
|
8
|
+
**Function sampling mode** (`data` is omitted/null, `fill` and/or
|
|
9
|
+
`fillOpacity` are `(x, y) => value` functions): the mark evaluates the
|
|
10
|
+
function on a pixel grid, converting pixel coords to data coords via scale
|
|
11
|
+
inversion, then maps results through the color scale.
|
|
12
|
+
|
|
13
|
+
**Scatter interpolation mode** (`data` is an array with `x`/`y` channels):
|
|
14
|
+
each datum contributes a position and fill value; the mark spatially
|
|
15
|
+
interpolates over the grid using the chosen `interpolate` method.
|
|
16
|
+
-->
|
|
17
|
+
<script lang="ts" generics="Datum extends DataRow">
|
|
18
|
+
interface RasterMarkProps {
|
|
19
|
+
/**
|
|
20
|
+
* Input data. For **dense grid** mode supply a flat row-major array and
|
|
21
|
+
* set `width`/`height`. Omit (or set null) for **function-sampling**
|
|
22
|
+
* mode. For **scatter interpolation** supply an array of records with
|
|
23
|
+
* `x`/`y` channels.
|
|
24
|
+
*/
|
|
25
|
+
data?: Datum[] | null;
|
|
26
|
+
/** x position channel (scatter interpolation mode) */
|
|
27
|
+
x?: ChannelAccessor<Datum>;
|
|
28
|
+
/** y position channel (scatter interpolation mode) */
|
|
29
|
+
y?: ChannelAccessor<Datum>;
|
|
30
|
+
/**
|
|
31
|
+
* fill color channel, identity function for dense grid, or an
|
|
32
|
+
* `(x, y) => value` function for function-sampling mode
|
|
33
|
+
*/
|
|
34
|
+
fill?: ChannelAccessor<Datum> | ((x: number, y: number) => any);
|
|
35
|
+
/**
|
|
36
|
+
* fill opacity channel or an `(x, y) => number` function for
|
|
37
|
+
* function-sampling mode
|
|
38
|
+
*/
|
|
39
|
+
fillOpacity?: ChannelAccessor<Datum> | ((x: number, y: number) => number);
|
|
40
|
+
/** left bound of the raster in data coordinates */
|
|
41
|
+
x1?: number;
|
|
42
|
+
/** top bound of the raster in data coordinates */
|
|
43
|
+
y1?: number;
|
|
44
|
+
/** right bound of the raster in data coordinates */
|
|
45
|
+
x2?: number;
|
|
46
|
+
/** bottom bound of the raster in data coordinates */
|
|
47
|
+
y2?: number;
|
|
48
|
+
/**
|
|
49
|
+
* explicit pixel-grid width; required for dense grid mode, also used
|
|
50
|
+
* to set the canvas resolution in other modes (overrides `pixelSize`)
|
|
51
|
+
*/
|
|
52
|
+
width?: number;
|
|
53
|
+
/**
|
|
54
|
+
* explicit pixel-grid height; required for dense grid mode, also used
|
|
55
|
+
* to set the canvas resolution in other modes (overrides `pixelSize`)
|
|
56
|
+
*/
|
|
57
|
+
height?: number;
|
|
58
|
+
/** pixel size in screen pixels (default 1); ignored when `width`/`height` are set */
|
|
59
|
+
pixelSize?: number;
|
|
60
|
+
/** Gaussian blur radius in grid pixels (default 0) */
|
|
61
|
+
blur?: number;
|
|
62
|
+
/**
|
|
63
|
+
* spatial interpolation for scatter mode:
|
|
64
|
+
* `"none"` | `"nearest"` | `"barycentric"` | `"random-walk"` or a
|
|
65
|
+
* custom `(index, w, h, X, Y, V) => W` function
|
|
66
|
+
*/
|
|
67
|
+
interpolate?: 'none' | 'nearest' | 'barycentric' | 'random-walk' | InterpolateFunction;
|
|
68
|
+
/** CSS image-rendering property (default `"auto"`) */
|
|
69
|
+
imageRendering?: string;
|
|
70
|
+
clipPath?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
import type {
|
|
74
|
+
DataRow,
|
|
75
|
+
DataRecord,
|
|
76
|
+
ChannelAccessor,
|
|
77
|
+
ScaledDataRecord,
|
|
78
|
+
MarkType
|
|
79
|
+
} from '../types/index.js';
|
|
80
|
+
import { blurImage, extent } from 'd3-array';
|
|
81
|
+
import { rgb } from 'd3-color';
|
|
82
|
+
import Mark from '../Mark.svelte';
|
|
83
|
+
import { usePlot } from '../hooks/usePlot.svelte.js';
|
|
84
|
+
import {
|
|
85
|
+
interpolateNone,
|
|
86
|
+
interpolateNearest,
|
|
87
|
+
interpolatorBarycentric,
|
|
88
|
+
interpolatorRandomWalk,
|
|
89
|
+
type InterpolateFunction
|
|
90
|
+
} from '../helpers/rasterInterpolate.js';
|
|
91
|
+
import { X, Y, RAW_VALUE } from '../transforms/recordize.js';
|
|
92
|
+
import { scaleLinear } from 'd3-scale';
|
|
93
|
+
|
|
94
|
+
let markProps: RasterMarkProps = $props();
|
|
95
|
+
|
|
96
|
+
const {
|
|
97
|
+
data,
|
|
98
|
+
fill,
|
|
99
|
+
fillOpacity,
|
|
100
|
+
x1: x1Prop,
|
|
101
|
+
y1: y1Prop,
|
|
102
|
+
x2: x2Prop,
|
|
103
|
+
y2: y2Prop,
|
|
104
|
+
width: widthProp,
|
|
105
|
+
height: heightProp,
|
|
106
|
+
pixelSize = 1,
|
|
107
|
+
blur = 0,
|
|
108
|
+
interpolate,
|
|
109
|
+
imageRendering = 'auto',
|
|
110
|
+
...options
|
|
111
|
+
}: RasterMarkProps = $derived({ ...markProps });
|
|
112
|
+
|
|
113
|
+
const plot = usePlot();
|
|
114
|
+
|
|
115
|
+
/** No data: fill/fillOpacity are (x,y) functions */
|
|
116
|
+
const isSamplerMode = $derived(data == null);
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Dense grid: data is a flat array, width+height are given, no x/y
|
|
120
|
+
* channels. Each datum is its own fill value (unless fill is specified).
|
|
121
|
+
*/
|
|
122
|
+
const isDenseGridMode = $derived(
|
|
123
|
+
data != null &&
|
|
124
|
+
widthProp != null &&
|
|
125
|
+
heightProp != null &&
|
|
126
|
+
(options as any).x == null &&
|
|
127
|
+
(options as any).y == null
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const interpolateFn = $derived(resolveInterpolate(interpolate));
|
|
131
|
+
|
|
132
|
+
function resolveInterpolate(interp: RasterMarkProps['interpolate']): InterpolateFunction {
|
|
133
|
+
if (typeof interp === 'function') return interp;
|
|
134
|
+
if (interp == null) return interpolateNone;
|
|
135
|
+
switch (String(interp).toLowerCase()) {
|
|
136
|
+
case 'none':
|
|
137
|
+
return interpolateNone;
|
|
138
|
+
case 'nearest':
|
|
139
|
+
return interpolateNearest;
|
|
140
|
+
case 'barycentric':
|
|
141
|
+
return interpolatorBarycentric();
|
|
142
|
+
case 'random-walk':
|
|
143
|
+
return interpolatorRandomWalk();
|
|
144
|
+
}
|
|
145
|
+
throw new Error(`invalid interpolate: ${interp}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Pixel-space bounds of the raster image. */
|
|
149
|
+
function getBounds() {
|
|
150
|
+
const facetWidth = plot.facetWidth ?? 100;
|
|
151
|
+
const facetHeight = plot.facetHeight ?? 100;
|
|
152
|
+
const marginLeft = plot.options.marginLeft ?? 0;
|
|
153
|
+
const marginTop = plot.options.marginTop ?? 0;
|
|
154
|
+
return {
|
|
155
|
+
bx1: marginLeft,
|
|
156
|
+
by1: marginTop,
|
|
157
|
+
bx2: marginLeft + facetWidth,
|
|
158
|
+
by2: marginTop + facetHeight
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Build the off-screen canvas and return it together with its bounds. */
|
|
163
|
+
function computeCanvas(scaledData: ScaledDataRecord[]): {
|
|
164
|
+
canvas: HTMLCanvasElement;
|
|
165
|
+
bx1: number;
|
|
166
|
+
by1: number;
|
|
167
|
+
dx: number;
|
|
168
|
+
dy: number;
|
|
169
|
+
} | null {
|
|
170
|
+
if (typeof document === 'undefined') return null;
|
|
171
|
+
|
|
172
|
+
const { bx1, by1, bx2, by2 } = getBounds();
|
|
173
|
+
const dx = bx2 - bx1;
|
|
174
|
+
const dy = by2 - by1;
|
|
175
|
+
// Canvas pixel dimensions
|
|
176
|
+
const w = widthProp ?? Math.round(Math.abs(dx) / pixelSize);
|
|
177
|
+
const h = heightProp ?? Math.round(Math.abs(dy) / pixelSize);
|
|
178
|
+
if (w <= 0 || h <= 0) return null;
|
|
179
|
+
const n = w * h;
|
|
180
|
+
|
|
181
|
+
// --- Populate fill value array F (and optional opacity array FO) ---
|
|
182
|
+
let F: any[] | null = null;
|
|
183
|
+
let FO: number[] | null = null;
|
|
184
|
+
|
|
185
|
+
if (isDenseGridMode) {
|
|
186
|
+
// Build the value array in data order, then flip rows so that
|
|
187
|
+
// data row 0 (y=0) renders at the bottom of the canvas, matching
|
|
188
|
+
// the standard y-up chart convention.
|
|
189
|
+
let Fraw: any[];
|
|
190
|
+
if (typeof fill === 'function') {
|
|
191
|
+
Fraw = (data as any[]).map(fill as (d: any) => any);
|
|
192
|
+
} else if (fill != null && typeof fill !== 'string') {
|
|
193
|
+
Fraw = (data as any[]).map((d) =>
|
|
194
|
+
typeof fill === 'string' ? d[fill] : (fill as (d: any) => any)(d)
|
|
195
|
+
);
|
|
196
|
+
} else {
|
|
197
|
+
Fraw = data as any[];
|
|
198
|
+
}
|
|
199
|
+
// Flip vertically: canvas row 0 (top) = data row h-1 (highest y).
|
|
200
|
+
F = new Array(n);
|
|
201
|
+
for (let row = 0; row < h; ++row) {
|
|
202
|
+
const srcRow = h - 1 - row;
|
|
203
|
+
for (let col = 0; col < w; ++col) {
|
|
204
|
+
F[row * w + col] = Fraw[srcRow * w + col];
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
} else if (isSamplerMode) {
|
|
208
|
+
// Evaluate f(x,y) for every grid pixel
|
|
209
|
+
const xScale = scaleLinear().range([x1Prop!, x2Prop!]).domain([bx1, bx2]);
|
|
210
|
+
const yScale = scaleLinear().range([y1Prop!, y2Prop!]).domain([by1, by2]);
|
|
211
|
+
const kx = dx / w;
|
|
212
|
+
const ky = dy / h;
|
|
213
|
+
if (typeof fill === 'function') {
|
|
214
|
+
F = new Array(n);
|
|
215
|
+
let i = 0;
|
|
216
|
+
for (let yi = 0.5; yi < h; ++yi) {
|
|
217
|
+
for (let xi = 0.5; xi < w; ++xi, ++i) {
|
|
218
|
+
const xData = xScale(bx1 + xi * kx);
|
|
219
|
+
const yData = yScale(by1 + yi * ky);
|
|
220
|
+
F[i] = (fill as (x: any, y: any) => any)(xData, yData);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (typeof fillOpacity === 'function') {
|
|
225
|
+
FO = new Array(n);
|
|
226
|
+
let i = 0;
|
|
227
|
+
for (let yi = 0.5; yi < h; ++yi) {
|
|
228
|
+
for (let xi = 0.5; xi < w; ++xi, ++i) {
|
|
229
|
+
const xData = xScale(bx1 + xi * kx);
|
|
230
|
+
const yData = yScale(by1 + yi * ky);
|
|
231
|
+
FO[i] = (fillOpacity as (x: any, y: any) => number)(xData, yData);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
} else if (scaledData.length > 0) {
|
|
236
|
+
// Scatter interpolation: map data points onto grid
|
|
237
|
+
const validData = scaledData.filter((d) => d.valid && d.x != null && d.y != null);
|
|
238
|
+
|
|
239
|
+
if (validData.length > 0) {
|
|
240
|
+
const kx = w / dx;
|
|
241
|
+
const ky = h / dy;
|
|
242
|
+
const index = validData.map((_, i) => i);
|
|
243
|
+
const IX = new Float64Array(validData.map((d) => ((d.x as number) - bx1) * kx));
|
|
244
|
+
const IY = new Float64Array(validData.map((d) => ((d.y as number) - by1) * ky));
|
|
245
|
+
// Use raw pre-color-scale fill values so we can interpolate
|
|
246
|
+
// numerically, then apply the color scale after interpolation.
|
|
247
|
+
const rawFill = validData.map((d) => d.resolved?.fill);
|
|
248
|
+
if (rawFill.some((v) => v != null)) {
|
|
249
|
+
F = Array.from(interpolateFn(index, w, h, IX, IY, rawFill));
|
|
250
|
+
}
|
|
251
|
+
if (fillOpacity !== undefined) {
|
|
252
|
+
const rawFO = validData.map((d) => d.resolved?.fillOpacity);
|
|
253
|
+
if (rawFO.some((v) => v != null)) {
|
|
254
|
+
FO = Array.from(interpolateFn(index, w, h, IX, IY, rawFO)) as number[];
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// --- Rasterize: map value array to RGBA pixel data ---
|
|
261
|
+
const canvas = document.createElement('canvas');
|
|
262
|
+
canvas.width = w;
|
|
263
|
+
canvas.height = h;
|
|
264
|
+
const ctx = canvas.getContext('2d')!;
|
|
265
|
+
const imageData = ctx.createImageData(w, h);
|
|
266
|
+
const imgData = imageData.data;
|
|
267
|
+
|
|
268
|
+
// For sampler mode the fill function values are not registered with the
|
|
269
|
+
// Plot's mark data (to avoid reactive cycles), so the color scale may
|
|
270
|
+
// not have a domain set. In that case we compute the F extent here and
|
|
271
|
+
// rescale through the plot color scale using a normalised [0,1] input.
|
|
272
|
+
let colorFn: (v: any) => any = plot.scales.color?.fn ?? ((x: any) => x);
|
|
273
|
+
if (isSamplerMode && F && plot.scales.color?.fn) {
|
|
274
|
+
const [fMin, fMax] = extent(F.filter((v) => v != null) as number[]);
|
|
275
|
+
const colorDomain = plot.scales.color.fn.domain?.() ?? [];
|
|
276
|
+
const domainIsEmpty =
|
|
277
|
+
colorDomain.length === 0 || colorDomain[0] === colorDomain[colorDomain.length - 1];
|
|
278
|
+
if (domainIsEmpty && fMin != null && fMax != null && fMin !== fMax) {
|
|
279
|
+
// Remap raw F values into the color scale's current domain range
|
|
280
|
+
const [dMin, dMax] =
|
|
281
|
+
colorDomain.length >= 2
|
|
282
|
+
? [colorDomain[0] as number, colorDomain[colorDomain.length - 1] as number]
|
|
283
|
+
: [0, 1];
|
|
284
|
+
const span = fMax - fMin;
|
|
285
|
+
colorFn = (v: any) =>
|
|
286
|
+
plot.scales.color!.fn(dMin + ((v - fMin) / span) * (dMax - dMin));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const defColor = typeof fill === 'string' ? rgb(fill) : null;
|
|
291
|
+
const defR = defColor?.r ?? 0;
|
|
292
|
+
const defG = defColor?.g ?? 0;
|
|
293
|
+
const defB = defColor?.b ?? 0;
|
|
294
|
+
const defA = typeof fillOpacity === 'number' ? fillOpacity * 255 : F ? 255 : 0;
|
|
295
|
+
|
|
296
|
+
for (let i = 0; i < n; ++i) {
|
|
297
|
+
const j = i << 2;
|
|
298
|
+
let r = defR,
|
|
299
|
+
g = defG,
|
|
300
|
+
b = defB,
|
|
301
|
+
a = defA;
|
|
302
|
+
|
|
303
|
+
if (F) {
|
|
304
|
+
const colorVal = colorFn(F[i]);
|
|
305
|
+
if (colorVal == null) {
|
|
306
|
+
imgData[j + 3] = 0;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
const c = rgb(String(colorVal));
|
|
310
|
+
if (c) ({ r, g, b } = c);
|
|
311
|
+
a = 255;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (FO != null) a = (FO[i] ?? 0) * 255;
|
|
315
|
+
|
|
316
|
+
imgData[j + 0] = r;
|
|
317
|
+
imgData[j + 1] = g;
|
|
318
|
+
imgData[j + 2] = b;
|
|
319
|
+
imgData[j + 3] = a;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (blur > 0) blurImage(imageData, blur);
|
|
323
|
+
ctx.putImageData(imageData, 0, 0);
|
|
324
|
+
|
|
325
|
+
return { canvas, bx1, by1, dx, dy };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Resolves the fill value for a single datum in dense grid mode.
|
|
330
|
+
* The datum may be a raw primitive or a record; `fill` may be a
|
|
331
|
+
* field name, accessor, or omitted (identity).
|
|
332
|
+
*/
|
|
333
|
+
function resolveFillValue(datum: any): any {
|
|
334
|
+
if (fill == null) return datum; // identity: datum IS the value
|
|
335
|
+
if (typeof fill === 'string') return datum[fill];
|
|
336
|
+
if (typeof fill === 'function') return (fill as (d: any) => any)(datum);
|
|
337
|
+
return datum;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* For dense grid mode, fold the flat array into records keyed by Symbols
|
|
342
|
+
* (like recordizeXY). Symbol keys survive `{ ...d, [INDEX]: i }` spreading
|
|
343
|
+
* in Mark.svelte, so scale domains are computed from the actual values.
|
|
344
|
+
* The grid column/row indices become x/y channels so the x and y scale
|
|
345
|
+
* domains are also registered (0..width-1 and 0..height-1).
|
|
346
|
+
*/
|
|
347
|
+
const denseMarkData = $derived(
|
|
348
|
+
isDenseGridMode
|
|
349
|
+
? (data as any[]).map((d, i) => ({
|
|
350
|
+
[X]: i % widthProp!,
|
|
351
|
+
[Y]: Math.floor(i / widthProp!),
|
|
352
|
+
[RAW_VALUE]: resolveFillValue(d)
|
|
353
|
+
}))
|
|
354
|
+
: null
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* For sampler mode, pass two corner records so the Plot can register the
|
|
359
|
+
* x/y scale domains from x1/y1/x2/y2. We intentionally do NOT pre-evaluate
|
|
360
|
+
* the fill function here — doing so inside a $derived creates a reactive
|
|
361
|
+
* cycle because the resulting array (new reference each evaluation) feeds
|
|
362
|
+
* back through Mark → plot.scales → scaledData → effect → mark.data.
|
|
363
|
+
* The color scale domain is handled directly inside computeCanvas instead.
|
|
364
|
+
*/
|
|
365
|
+
const samplerMarkData = $derived.by(() => {
|
|
366
|
+
if (!isSamplerMode) return null;
|
|
367
|
+
const x1 = x1Prop,
|
|
368
|
+
x2 = x2Prop,
|
|
369
|
+
y1 = y1Prop,
|
|
370
|
+
y2 = y2Prop;
|
|
371
|
+
if (x1 == null || x2 == null || y1 == null || y2 == null) return null;
|
|
372
|
+
return [
|
|
373
|
+
{ [X]: x1, [Y]: y1 },
|
|
374
|
+
{ [X]: x2, [Y]: y2 }
|
|
375
|
+
] as unknown as DataRecord[];
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const markChannels = $derived(
|
|
379
|
+
isDenseGridMode
|
|
380
|
+
? (['x', 'y', 'fill'] as const)
|
|
381
|
+
: isSamplerMode
|
|
382
|
+
? (['x', 'y'] as const)
|
|
383
|
+
: (['x', 'y', 'fill', 'fillOpacity'] as const)
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
const markFill = $derived(
|
|
387
|
+
isDenseGridMode ? (RAW_VALUE as any) : !isSamplerMode ? (fill as any) : undefined
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
const markX = $derived(isDenseGridMode || isSamplerMode ? (X as any) : undefined);
|
|
391
|
+
const markY = $derived(isDenseGridMode || isSamplerMode ? (Y as any) : undefined);
|
|
392
|
+
</script>
|
|
393
|
+
|
|
394
|
+
<Mark
|
|
395
|
+
type={'raster' as MarkType}
|
|
396
|
+
data={isDenseGridMode
|
|
397
|
+
? (denseMarkData as DataRecord[])
|
|
398
|
+
: isSamplerMode
|
|
399
|
+
? ((samplerMarkData ?? []) as DataRecord[])
|
|
400
|
+
: ((data ?? []) as DataRecord[])}
|
|
401
|
+
channels={markChannels as any}
|
|
402
|
+
x={markX}
|
|
403
|
+
y={markY}
|
|
404
|
+
fill={markFill}
|
|
405
|
+
fillOpacity={!isSamplerMode && !isDenseGridMode ? (fillOpacity as any) : undefined}
|
|
406
|
+
{...options}>
|
|
407
|
+
{#snippet children({ scaledData })}
|
|
408
|
+
{@const result = computeCanvas(scaledData)}
|
|
409
|
+
{#if result}
|
|
410
|
+
<image
|
|
411
|
+
transform="translate({plot.options.marginLeft},{plot.options
|
|
412
|
+
.marginTop}) scale({Math.sign(result.dx)},{Math.sign(result.dy)})"
|
|
413
|
+
width={Math.abs(result.dx)}
|
|
414
|
+
height={Math.abs(result.dy)}
|
|
415
|
+
preserveAspectRatio="none"
|
|
416
|
+
clip-path={options.clipPath}
|
|
417
|
+
image-rendering={imageRendering}
|
|
418
|
+
href={result.canvas.toDataURL()} />
|
|
419
|
+
{/if}
|
|
420
|
+
{/snippet}
|
|
421
|
+
</Mark>
|
|
@@ -0,0 +1,95 @@
|
|
|
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 interpolation mode) */
|
|
13
|
+
x?: ChannelAccessor<Datum>;
|
|
14
|
+
/** y position channel (scatter interpolation mode) */
|
|
15
|
+
y?: ChannelAccessor<Datum>;
|
|
16
|
+
/**
|
|
17
|
+
* fill color channel, identity function for dense grid, or an
|
|
18
|
+
* `(x, y) => value` function for function-sampling mode
|
|
19
|
+
*/
|
|
20
|
+
fill?: ChannelAccessor<Datum> | ((x: number, y: number) => any);
|
|
21
|
+
/**
|
|
22
|
+
* fill opacity channel or an `(x, y) => number` function for
|
|
23
|
+
* function-sampling mode
|
|
24
|
+
*/
|
|
25
|
+
fillOpacity?: ChannelAccessor<Datum> | ((x: number, y: number) => number);
|
|
26
|
+
/** left bound of the raster in data coordinates */
|
|
27
|
+
x1?: number;
|
|
28
|
+
/** top bound of the raster in data coordinates */
|
|
29
|
+
y1?: number;
|
|
30
|
+
/** right bound of the raster in data coordinates */
|
|
31
|
+
x2?: number;
|
|
32
|
+
/** bottom bound of the raster in data coordinates */
|
|
33
|
+
y2?: number;
|
|
34
|
+
/**
|
|
35
|
+
* explicit pixel-grid width; required for dense grid mode, also used
|
|
36
|
+
* to set the canvas resolution in other modes (overrides `pixelSize`)
|
|
37
|
+
*/
|
|
38
|
+
width?: number;
|
|
39
|
+
/**
|
|
40
|
+
* explicit pixel-grid height; required for dense grid mode, also used
|
|
41
|
+
* to set the canvas resolution in other modes (overrides `pixelSize`)
|
|
42
|
+
*/
|
|
43
|
+
height?: number;
|
|
44
|
+
/** pixel size in screen pixels (default 1); ignored when `width`/`height` are set */
|
|
45
|
+
pixelSize?: number;
|
|
46
|
+
/** Gaussian blur radius in grid pixels (default 0) */
|
|
47
|
+
blur?: number;
|
|
48
|
+
/**
|
|
49
|
+
* spatial interpolation for scatter mode:
|
|
50
|
+
* `"none"` | `"nearest"` | `"barycentric"` | `"random-walk"` or a
|
|
51
|
+
* custom `(index, w, h, X, Y, V) => W` function
|
|
52
|
+
*/
|
|
53
|
+
interpolate?: "none" | "nearest" | "barycentric" | "random-walk" | InterpolateFunction;
|
|
54
|
+
/** CSS image-rendering property (default `"auto"`) */
|
|
55
|
+
imageRendering?: string;
|
|
56
|
+
clipPath?: string;
|
|
57
|
+
};
|
|
58
|
+
exports: {};
|
|
59
|
+
bindings: "";
|
|
60
|
+
slots: {};
|
|
61
|
+
events: {};
|
|
62
|
+
};
|
|
63
|
+
declare class __sveltets_Render<Datum extends DataRow> {
|
|
64
|
+
props(): ReturnType<typeof $$render<Datum>>['props'];
|
|
65
|
+
events(): ReturnType<typeof $$render<Datum>>['events'];
|
|
66
|
+
slots(): ReturnType<typeof $$render<Datum>>['slots'];
|
|
67
|
+
bindings(): "";
|
|
68
|
+
exports(): {};
|
|
69
|
+
}
|
|
70
|
+
interface $$IsomorphicComponent {
|
|
71
|
+
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']>> & {
|
|
72
|
+
$$bindings?: ReturnType<__sveltets_Render<Datum>['bindings']>;
|
|
73
|
+
} & ReturnType<__sveltets_Render<Datum>['exports']>;
|
|
74
|
+
<Datum extends DataRow>(internal: unknown, props: ReturnType<__sveltets_Render<Datum>['props']> & {}): ReturnType<__sveltets_Render<Datum>['exports']>;
|
|
75
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Renders a raster image in one of three modes:
|
|
79
|
+
*
|
|
80
|
+
* **Dense grid mode** (`data` is an array, `width` and `height` are set, no
|
|
81
|
+
* `x`/`y` channels): the data is treated as a row-major grid of fill values.
|
|
82
|
+
* `fill` defaults to the identity function (each datum is its own value).
|
|
83
|
+
*
|
|
84
|
+
* **Function sampling mode** (`data` is omitted/null, `fill` and/or
|
|
85
|
+
* `fillOpacity` are `(x, y) => value` functions): the mark evaluates the
|
|
86
|
+
* function on a pixel grid, converting pixel coords to data coords via scale
|
|
87
|
+
* inversion, then maps results through the color scale.
|
|
88
|
+
*
|
|
89
|
+
* **Scatter interpolation mode** (`data` is an array with `x`/`y` channels):
|
|
90
|
+
* each datum contributes a position and fill value; the mark spatially
|
|
91
|
+
* interpolates over the grid using the chosen `interpolate` method.
|
|
92
|
+
*/
|
|
93
|
+
declare const Raster: $$IsomorphicComponent;
|
|
94
|
+
type Raster<Datum extends DataRow> = InstanceType<typeof Raster<Datum>>;
|
|
95
|
+
export default Raster;
|
package/dist/marks/Text.svelte
CHANGED
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
|
|
110
110
|
import MultilineText from './helpers/MultilineText.svelte';
|
|
111
111
|
import TextCanvas from './helpers/TextCanvas.svelte';
|
|
112
|
-
import { indexData } from '../transforms/recordize.js';
|
|
112
|
+
import { indexData, recordize } from '../transforms/recordize.js';
|
|
113
113
|
import { getPlotDefaults } from '../hooks/plotDefaults.js';
|
|
114
114
|
|
|
115
115
|
const DEFAULTS = {
|
|
@@ -137,11 +137,13 @@
|
|
|
137
137
|
} = $derived(mergedProps);
|
|
138
138
|
|
|
139
139
|
const args = $derived(
|
|
140
|
-
sort(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
140
|
+
sort(
|
|
141
|
+
recordize({
|
|
142
|
+
data,
|
|
143
|
+
...options
|
|
144
|
+
} as any)
|
|
145
|
+
)
|
|
146
|
+
) as unknown as TextMarkProps;
|
|
145
147
|
</script>
|
|
146
148
|
|
|
147
149
|
<Mark
|
|
@@ -5,7 +5,12 @@
|
|
|
5
5
|
children,
|
|
6
6
|
class: className = null,
|
|
7
7
|
...groupProps
|
|
8
|
-
}: {
|
|
8
|
+
}: {
|
|
9
|
+
children: Snippet;
|
|
10
|
+
length?: number;
|
|
11
|
+
class?: string | null;
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
} = $props();
|
|
9
14
|
</script>
|
|
10
15
|
|
|
11
16
|
{#if length > 1 || className}
|
package/dist/marks/index.d.ts
CHANGED
|
@@ -33,6 +33,7 @@ export { default as LineX } from './LineX.svelte';
|
|
|
33
33
|
export { default as LineY } from './LineY.svelte';
|
|
34
34
|
export { default as Link } from './Link.svelte';
|
|
35
35
|
export { default as Pointer } from './Pointer.svelte';
|
|
36
|
+
export { default as Raster } from './Raster.svelte';
|
|
36
37
|
export { default as Rect } from './Rect.svelte';
|
|
37
38
|
export { default as RectX } from './RectX.svelte';
|
|
38
39
|
export { default as RectY } from './RectY.svelte';
|
package/dist/marks/index.js
CHANGED
|
@@ -33,6 +33,7 @@ export { default as LineX } from './LineX.svelte';
|
|
|
33
33
|
export { default as LineY } from './LineY.svelte';
|
|
34
34
|
export { default as Link } from './Link.svelte';
|
|
35
35
|
export { default as Pointer } from './Pointer.svelte';
|
|
36
|
+
export { default as Raster } from './Raster.svelte';
|
|
36
37
|
export { default as Rect } from './Rect.svelte';
|
|
37
38
|
export { default as RectX } from './RectX.svelte';
|
|
38
39
|
export { default as RectY } from './RectY.svelte';
|
package/dist/types/mark.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export type Mark<T> = {
|
|
|
6
6
|
data: DataRecord<T>[];
|
|
7
7
|
options: T;
|
|
8
8
|
};
|
|
9
|
-
export type MarkType = 'area' | 'arrow' | 'axisX' | 'axisY' | 'barX' | 'barY' | 'cell' | 'custom' | 'dot' | 'vector' | 'frame' | 'geo' | 'gridX' | 'gridY' | 'image' | 'link' | 'line' | 'rect' | 'regression' | 'ruleX' | 'ruleY' | 'swoopyArrow' | 'text' | 'tickX' | 'tickY' | 'trail' | 'waffleX' | 'waffleY';
|
|
9
|
+
export type MarkType = 'area' | 'arrow' | 'axisX' | 'axisY' | 'barX' | 'barY' | 'cell' | 'custom' | 'dot' | 'vector' | 'frame' | 'geo' | 'gridX' | 'gridY' | 'image' | 'link' | 'line' | 'raster' | 'rect' | 'regression' | 'ruleX' | 'ruleY' | 'swoopyArrow' | 'text' | 'tickX' | 'tickY' | 'trail' | 'waffleX' | 'waffleY';
|
|
10
10
|
export type MarkStyleProps = 'strokeDasharray' | 'strokeLinejoin' | 'strokeLinecap' | 'opacity' | 'cursor' | 'pointerEvents' | 'blend' | 'fill' | 'fillOpacity' | 'fontFamily' | 'fontWeight' | 'fontVariant' | 'fontSize' | 'fontStyle' | 'letterSpacing' | 'wordSpacing' | 'stroke' | 'strokeWidth' | 'strokeOpacity' | 'x' | 'y' | 'clipPath' | 'mask' | 'filter' | 'angle' | 'radius' | 'symbol' | 'textAnchor' | 'textTransform' | 'textDecoration' | 'width';
|
|
11
11
|
import type { ChannelAccessor, ConstantAccessor, DataRecord, RawValue } from './index.js';
|
|
12
12
|
import type * as CSS from 'csstype';
|
package/dist/types/plot.d.ts
CHANGED
|
@@ -84,7 +84,14 @@ export type PlotDefaults = {
|
|
|
84
84
|
* default color scheme
|
|
85
85
|
*/
|
|
86
86
|
colorScheme: ColorScheme;
|
|
87
|
-
|
|
87
|
+
/**
|
|
88
|
+
* default color scheme for type 'categorical'
|
|
89
|
+
*/
|
|
90
|
+
categoricalColorScheme: ColorScheme | string[] | ColorScheme | string[] | Record<string, string>;
|
|
91
|
+
/**
|
|
92
|
+
* default color scheme for tyoe 'diverging'
|
|
93
|
+
*/
|
|
94
|
+
divergingColorScheme: ColorScheme | string[];
|
|
88
95
|
/**
|
|
89
96
|
* fallback color to be used for null/NA
|
|
90
97
|
*/
|
|
@@ -402,6 +409,8 @@ export type PlotOptions = {
|
|
|
402
409
|
projection: string | null | {
|
|
403
410
|
type?: string;
|
|
404
411
|
rotate?: [number, number] | [number, number, number];
|
|
412
|
+
center?: [number, number] | [number, number, number];
|
|
413
|
+
parallels?: [number, number] | [number, number, number];
|
|
405
414
|
domain?: object;
|
|
406
415
|
inset?: number;
|
|
407
416
|
clip?: Clip;
|