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
package/dist/marks/Geo.svelte
CHANGED
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
import Anchor from './helpers/Anchor.svelte';
|
|
46
46
|
import { getPlotDefaults } from '../hooks/plotDefaults.js';
|
|
47
47
|
import { usePlot } from '../hooks/usePlot.svelte.js';
|
|
48
|
+
import GroupMultiple from './helpers/GroupMultiple.svelte';
|
|
48
49
|
|
|
49
50
|
const plot = usePlot();
|
|
50
51
|
|
|
@@ -85,6 +86,8 @@
|
|
|
85
86
|
})
|
|
86
87
|
)
|
|
87
88
|
);
|
|
89
|
+
|
|
90
|
+
const classes = $derived(['geo', geoType && `geo-${geoType}`, className]);
|
|
88
91
|
</script>
|
|
89
92
|
|
|
90
93
|
<Mark
|
|
@@ -92,10 +95,10 @@
|
|
|
92
95
|
channels={['fill', 'stroke', 'opacity', 'fillOpacity', 'strokeOpacity', 'r']}
|
|
93
96
|
{...args}>
|
|
94
97
|
{#snippet children({ mark, scaledData, usedScales })}
|
|
95
|
-
<
|
|
98
|
+
<GroupMultiple
|
|
96
99
|
aria-label="geo"
|
|
97
|
-
class={
|
|
98
|
-
|
|
100
|
+
class={scaledData.length > 1 ? classes.filter(Boolean).join(' ') : null}
|
|
101
|
+
length={scaledData.length}>
|
|
99
102
|
{#if canvas}
|
|
100
103
|
<GeoCanvas data={scaledData} {path} {mark} {usedScales} />
|
|
101
104
|
{:else}
|
|
@@ -116,7 +119,8 @@
|
|
|
116
119
|
<path
|
|
117
120
|
d={path(geometry as any)}
|
|
118
121
|
{style}
|
|
119
|
-
|
|
122
|
+
aria-label="geo"
|
|
123
|
+
class={[scaledData.length > 1 ? null : classes, styleClass]}
|
|
120
124
|
filter={resolveProp(args.svgFilter, d.datum, undefined) as
|
|
121
125
|
| string
|
|
122
126
|
| undefined}
|
|
@@ -131,6 +135,6 @@
|
|
|
131
135
|
{/if}
|
|
132
136
|
{/each}
|
|
133
137
|
{/if}
|
|
134
|
-
</
|
|
138
|
+
</GroupMultiple>
|
|
135
139
|
{/snippet}
|
|
136
140
|
</Mark>
|
package/dist/marks/Line.svelte
CHANGED
|
@@ -94,28 +94,65 @@
|
|
|
94
94
|
const args = $derived(sort(recordizeXY({ data, ...options })));
|
|
95
95
|
|
|
96
96
|
/**
|
|
97
|
-
* Groups the data by the specified key
|
|
97
|
+
* Groups the data by the specified key (and optionally a secondary key).
|
|
98
|
+
* When a secondary key is provided, each primary group is further split by
|
|
99
|
+
* the secondary key, with each sub-segment extended to include the first point
|
|
100
|
+
* of the next sub-segment so consecutive segments share an endpoint (enabling
|
|
101
|
+
* multi-colored lines without gaps).
|
|
98
102
|
*/
|
|
99
|
-
function groupIndex(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
function groupIndex(
|
|
104
|
+
data: ScaledDataRecord[],
|
|
105
|
+
groupByKey: ChannelAccessor<Datum> | null,
|
|
106
|
+
secondaryKey: ChannelAccessor<Datum> | null = null
|
|
107
|
+
) {
|
|
108
|
+
if (!groupByKey && !secondaryKey) return [data];
|
|
109
|
+
|
|
110
|
+
// Group by the primary key
|
|
111
|
+
const primaryGroups: ScaledDataRecord[][] = [];
|
|
112
|
+
let primaryGroup: ScaledDataRecord[] = [];
|
|
113
|
+
let lastPrimaryValue: unknown;
|
|
104
114
|
for (const d of data) {
|
|
105
|
-
const
|
|
106
|
-
if (
|
|
107
|
-
|
|
115
|
+
const primaryValue = resolveProp(groupByKey!, d.datum);
|
|
116
|
+
if (primaryValue === lastPrimaryValue) {
|
|
117
|
+
primaryGroup.push(d);
|
|
108
118
|
} else {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
119
|
+
primaryGroup = [d];
|
|
120
|
+
primaryGroups.push(primaryGroup);
|
|
121
|
+
lastPrimaryValue = primaryValue;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!secondaryKey) return primaryGroups;
|
|
126
|
+
|
|
127
|
+
// Further split each primary group by the secondary key. Each sub-segment is
|
|
128
|
+
// extended to include the first point of the next sub-segment so that
|
|
129
|
+
// consecutive segments share an endpoint (no gaps in multi-colored lines).
|
|
130
|
+
const result: ScaledDataRecord[][] = [];
|
|
131
|
+
for (const pGroup of primaryGroups) {
|
|
132
|
+
if (pGroup.length === 0) continue;
|
|
133
|
+
let subGroup: ScaledDataRecord[] = [pGroup[0]];
|
|
134
|
+
let lastSecondaryValue = resolveProp(secondaryKey, pGroup[0].datum);
|
|
135
|
+
for (let i = 1; i < pGroup.length; i++) {
|
|
136
|
+
const d = pGroup[i];
|
|
137
|
+
const secondaryValue = resolveProp(secondaryKey, d.datum);
|
|
138
|
+
if (secondaryValue === lastSecondaryValue) {
|
|
139
|
+
subGroup.push(d);
|
|
140
|
+
} else {
|
|
141
|
+
subGroup.push(d); // extend to connect to next sub-segment
|
|
142
|
+
result.push(subGroup);
|
|
143
|
+
subGroup = [d]; // new sub-segment begins here
|
|
144
|
+
lastSecondaryValue = secondaryValue;
|
|
145
|
+
}
|
|
113
146
|
}
|
|
147
|
+
result.push(subGroup);
|
|
114
148
|
}
|
|
115
|
-
return
|
|
149
|
+
return result;
|
|
116
150
|
}
|
|
117
151
|
|
|
118
152
|
const groupByKey = $derived(args.z || args.stroke) as ChannelAccessor<Datum> | null;
|
|
153
|
+
const secondaryKey = $derived(
|
|
154
|
+
args.z && args.stroke ? args.stroke : null
|
|
155
|
+
) as ChannelAccessor<Datum> | null;
|
|
119
156
|
|
|
120
157
|
const plot = usePlot();
|
|
121
158
|
|
|
@@ -155,7 +192,7 @@
|
|
|
155
192
|
{...args}>
|
|
156
193
|
{#snippet children({ mark, usedScales, scaledData })}
|
|
157
194
|
{#if scaledData.length > 0}
|
|
158
|
-
{@const groupedLineData = groupIndex(scaledData, groupByKey)}
|
|
195
|
+
{@const groupedLineData = groupIndex(scaledData, groupByKey, secondaryKey)}
|
|
159
196
|
{#if canvas}
|
|
160
197
|
<LineCanvas {groupedLineData} {mark} {usedScales} {linePath} {groupByKey} />
|
|
161
198
|
{:else}
|
|
@@ -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
|