layerchart 2.0.0-next.50 → 2.0.0-next.51
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/components/Axis.svelte +25 -0
- package/dist/components/Axis.svelte.d.ts +10 -0
- package/dist/components/Circle.svelte +82 -59
- package/dist/components/Ellipse.svelte +83 -64
- package/dist/components/GeoRaster.svelte +311 -0
- package/dist/components/GeoRaster.svelte.d.ts +61 -0
- package/dist/components/Grid.svelte +15 -0
- package/dist/components/Grid.svelte.d.ts +5 -0
- package/dist/components/Image.svelte +2 -2
- package/dist/components/Line.svelte +82 -62
- package/dist/components/Points.svelte +2 -2
- package/dist/components/Polygon.svelte +92 -56
- package/dist/components/Rect.svelte +113 -64
- package/dist/components/Rule.svelte +2 -0
- package/dist/components/Sankey.svelte +0 -2
- package/dist/components/Text.svelte +83 -52
- package/dist/components/charts/PieChart.svelte +2 -2
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.js +2 -0
- package/dist/components/layers/Canvas.svelte +65 -48
- package/dist/components/layers/Canvas.svelte.d.ts +10 -0
- package/dist/contexts/canvas.d.ts +3 -0
- package/dist/server/ContextCapture.svelte +30 -0
- package/dist/server/ContextCapture.svelte.d.ts +8 -0
- package/dist/server/ServerChart.svelte +26 -0
- package/dist/server/ServerChart.svelte.d.ts +11 -0
- package/dist/server/TestBarChart.svelte +35 -0
- package/dist/server/TestBarChart.svelte.d.ts +14 -0
- package/dist/server/TestLineChart.svelte +35 -0
- package/dist/server/TestLineChart.svelte.d.ts +14 -0
- package/dist/server/captureStore.d.ts +8 -0
- package/dist/server/captureStore.js +18 -0
- package/dist/server/index.d.ts +137 -0
- package/dist/server/index.js +141 -0
- package/dist/server/renderChart.ssr.test.d.ts +1 -0
- package/dist/server/renderChart.ssr.test.js +205 -0
- package/dist/server/renderTree.d.ts +8 -0
- package/dist/server/renderTree.js +29 -0
- package/dist/states/__screenshots__/chart.svelte.test.ts/ChartState-geo-projection-skips-markInfo-should-not-derive-x-y-accessors-from-marks-when-geo-projection-is-active-1.png +0 -0
- package/dist/states/__screenshots__/chart.svelte.test.ts/ChartState-geo-projection-skips-markInfo-should-not-derive-x-y-accessors-from-marks-when-geo-projection-is-active-2.png +0 -0
- package/dist/states/chart.svelte.d.ts +5 -1
- package/dist/states/chart.svelte.js +18 -3
- package/dist/states/chart.svelte.test.js +110 -0
- package/dist/states/geo.svelte.d.ts +5 -1
- package/dist/states/geo.svelte.js +80 -68
- package/dist/utils/canvas.js +29 -10
- package/dist/utils/canvas.svelte.test.js +2 -2
- package/dist/utils/motion.svelte.js +14 -0
- package/package.json +7 -1
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { GeoProjection } from 'd3-geo';
|
|
3
|
+
|
|
4
|
+
export type GeoRasterPropsWithoutHTML = {
|
|
5
|
+
/**
|
|
6
|
+
* The image to reproject. Either:
|
|
7
|
+
*
|
|
8
|
+
* - a URL string (loaded via `Image` with `crossOrigin="anonymous"`),
|
|
9
|
+
* - a preloaded `HTMLImageElement`,
|
|
10
|
+
* - an `HTMLCanvasElement` (e.g. a stitched tile mosaic),
|
|
11
|
+
* - or an `ImageBitmap`.
|
|
12
|
+
*
|
|
13
|
+
* The image pixel data must be readable (not tainted by CORS). When passing a
|
|
14
|
+
* URL, the component sets `crossOrigin` on the underlying `Image` — the host
|
|
15
|
+
* must respond with the appropriate `Access-Control-Allow-Origin` header.
|
|
16
|
+
*/
|
|
17
|
+
image: string | HTMLImageElement | HTMLCanvasElement | ImageBitmap;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Projection used by the source image. Pass as an uncalled function, e.g.
|
|
21
|
+
* `sourceProjection={geoEquirectangular}`.
|
|
22
|
+
*
|
|
23
|
+
* When omitted, the source is assumed to be an equirectangular (plate carrée)
|
|
24
|
+
* image covering the full globe — i.e. `x = ((lon + 180) / 360) * width`,
|
|
25
|
+
* `y = ((90 - lat) / 180) * height`. This is the common layout for imagery
|
|
26
|
+
* such as NASA Blue Marble / Black Marble.
|
|
27
|
+
*/
|
|
28
|
+
sourceProjection?: () => GeoProjection;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Pixel sampling strategy.
|
|
32
|
+
*
|
|
33
|
+
* - `nearest` (default): fastest, slightly blocky at low source resolution.
|
|
34
|
+
* - `bilinear`: smoother, ~4x slower per pixel.
|
|
35
|
+
*
|
|
36
|
+
* @default 'nearest'
|
|
37
|
+
*/
|
|
38
|
+
interpolate?: 'nearest' | 'bilinear';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* `crossOrigin` attribute applied to the underlying `Image` element when
|
|
42
|
+
* loading from a URL. Set to `null` to disable.
|
|
43
|
+
*
|
|
44
|
+
* @default 'anonymous'
|
|
45
|
+
*/
|
|
46
|
+
crossOrigin?: string | null;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Resolution multiplier for the reprojected output, relative to chart CSS
|
|
50
|
+
* pixels. A value greater than 1 produces a sharper image on HiDPI
|
|
51
|
+
* displays at the cost of more work per frame.
|
|
52
|
+
*
|
|
53
|
+
* @default 1
|
|
54
|
+
*/
|
|
55
|
+
resolution?: number;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Disable image smoothing when compositing the reprojected buffer onto the
|
|
59
|
+
* destination canvas. Useful for a pixel-art look.
|
|
60
|
+
*
|
|
61
|
+
* @default false
|
|
62
|
+
*/
|
|
63
|
+
pixelated?: boolean;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export type GeoRasterProps = GeoRasterPropsWithoutHTML;
|
|
67
|
+
</script>
|
|
68
|
+
|
|
69
|
+
<script lang="ts">
|
|
70
|
+
import { untrack } from 'svelte';
|
|
71
|
+
import { geoPath } from 'd3-geo';
|
|
72
|
+
|
|
73
|
+
import { getChartContext } from '../contexts/chart.js';
|
|
74
|
+
import { getGeoContext } from '../contexts/geo.js';
|
|
75
|
+
import { getLayerContext } from '../contexts/layer.js';
|
|
76
|
+
|
|
77
|
+
let {
|
|
78
|
+
image,
|
|
79
|
+
sourceProjection,
|
|
80
|
+
interpolate = 'nearest',
|
|
81
|
+
crossOrigin = 'anonymous',
|
|
82
|
+
resolution = 1,
|
|
83
|
+
pixelated = false,
|
|
84
|
+
}: GeoRasterProps = $props();
|
|
85
|
+
|
|
86
|
+
const ctx = getChartContext();
|
|
87
|
+
const geo = getGeoContext();
|
|
88
|
+
const layerCtx = getLayerContext();
|
|
89
|
+
|
|
90
|
+
type SourcePixels = {
|
|
91
|
+
data: Uint8ClampedArray;
|
|
92
|
+
width: number;
|
|
93
|
+
height: number;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
let source = $state<SourcePixels | null>(null);
|
|
97
|
+
|
|
98
|
+
$effect(() => {
|
|
99
|
+
if (typeof window === 'undefined') return;
|
|
100
|
+
// Re-run when `image` changes
|
|
101
|
+
const src = image;
|
|
102
|
+
|
|
103
|
+
let cancelled = false;
|
|
104
|
+
|
|
105
|
+
async function load() {
|
|
106
|
+
let drawable: CanvasImageSource;
|
|
107
|
+
let sw: number;
|
|
108
|
+
let sh: number;
|
|
109
|
+
|
|
110
|
+
if (typeof src === 'string') {
|
|
111
|
+
const img = new Image();
|
|
112
|
+
if (crossOrigin !== null) img.crossOrigin = crossOrigin;
|
|
113
|
+
img.src = src;
|
|
114
|
+
try {
|
|
115
|
+
await img.decode();
|
|
116
|
+
} catch (err) {
|
|
117
|
+
console.warn('[GeoRaster] Failed to decode image', err);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
drawable = img;
|
|
121
|
+
sw = img.naturalWidth;
|
|
122
|
+
sh = img.naturalHeight;
|
|
123
|
+
} else if (src instanceof HTMLImageElement) {
|
|
124
|
+
try {
|
|
125
|
+
await src.decode();
|
|
126
|
+
} catch (err) {
|
|
127
|
+
console.warn('[GeoRaster] Failed to decode image', err);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
drawable = src;
|
|
131
|
+
sw = src.naturalWidth;
|
|
132
|
+
sh = src.naturalHeight;
|
|
133
|
+
} else {
|
|
134
|
+
// HTMLCanvasElement or ImageBitmap — already decoded
|
|
135
|
+
drawable = src;
|
|
136
|
+
sw = src.width;
|
|
137
|
+
sh = src.height;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (cancelled) return;
|
|
141
|
+
if (!sw || !sh) return;
|
|
142
|
+
|
|
143
|
+
const offscreen = document.createElement('canvas');
|
|
144
|
+
offscreen.width = sw;
|
|
145
|
+
offscreen.height = sh;
|
|
146
|
+
const octx = offscreen.getContext('2d', { willReadFrequently: true });
|
|
147
|
+
if (!octx) return;
|
|
148
|
+
octx.drawImage(drawable, 0, 0);
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const imageData = octx.getImageData(0, 0, sw, sh);
|
|
152
|
+
untrack(() => {
|
|
153
|
+
source = { data: imageData.data, width: sw, height: sh };
|
|
154
|
+
});
|
|
155
|
+
} catch (err) {
|
|
156
|
+
console.warn(
|
|
157
|
+
'[GeoRaster] Unable to read image pixels — the image is likely tainted by CORS',
|
|
158
|
+
err
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
load();
|
|
164
|
+
|
|
165
|
+
return () => {
|
|
166
|
+
cancelled = true;
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
function render(dstCtx: CanvasRenderingContext2D) {
|
|
171
|
+
const src = source;
|
|
172
|
+
if (!src) return;
|
|
173
|
+
|
|
174
|
+
const projection = geo.projection;
|
|
175
|
+
if (!projection?.invert) return;
|
|
176
|
+
|
|
177
|
+
const width = Math.max(1, Math.floor(ctx.width * resolution));
|
|
178
|
+
const height = Math.max(1, Math.floor(ctx.height * resolution));
|
|
179
|
+
if (width <= 0 || height <= 0) return;
|
|
180
|
+
|
|
181
|
+
const buffer = document.createElement('canvas');
|
|
182
|
+
buffer.width = width;
|
|
183
|
+
buffer.height = height;
|
|
184
|
+
const bctx = buffer.getContext('2d');
|
|
185
|
+
if (!bctx) return;
|
|
186
|
+
|
|
187
|
+
const dstImage = bctx.createImageData(width, height);
|
|
188
|
+
const dstData = dstImage.data;
|
|
189
|
+
|
|
190
|
+
const srcData = src.data;
|
|
191
|
+
const sw = src.width;
|
|
192
|
+
const sh = src.height;
|
|
193
|
+
|
|
194
|
+
const srcProj = sourceProjection?.();
|
|
195
|
+
const invert = projection.invert.bind(projection);
|
|
196
|
+
const bilinear = interpolate === 'bilinear';
|
|
197
|
+
|
|
198
|
+
for (let y = 0; y < height; y++) {
|
|
199
|
+
for (let x = 0; x < width; x++) {
|
|
200
|
+
// Inverse-project destination pixel (in chart-space coordinates) to lon/lat.
|
|
201
|
+
// When `resolution !== 1`, scale the sample point back to chart coordinates.
|
|
202
|
+
const lonlat = invert([x / resolution, y / resolution]);
|
|
203
|
+
if (!lonlat) continue;
|
|
204
|
+
|
|
205
|
+
const lon = lonlat[0];
|
|
206
|
+
const lat = lonlat[1];
|
|
207
|
+
if (!Number.isFinite(lon) || !Number.isFinite(lat)) continue;
|
|
208
|
+
|
|
209
|
+
let sx: number;
|
|
210
|
+
let sy: number;
|
|
211
|
+
|
|
212
|
+
if (srcProj) {
|
|
213
|
+
const p = srcProj([lon, lat]);
|
|
214
|
+
if (!p) continue;
|
|
215
|
+
sx = p[0];
|
|
216
|
+
sy = p[1];
|
|
217
|
+
} else {
|
|
218
|
+
// Equirectangular source (plate carrée) — the default for most
|
|
219
|
+
// full-globe imagery like NASA Blue Marble.
|
|
220
|
+
sx = ((lon + 180) / 360) * sw;
|
|
221
|
+
sy = ((90 - lat) / 180) * sh;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (!(sx >= 0 && sx < sw && sy >= 0 && sy < sh)) continue;
|
|
225
|
+
|
|
226
|
+
const di = (y * width + x) * 4;
|
|
227
|
+
|
|
228
|
+
if (bilinear) {
|
|
229
|
+
const x0 = Math.floor(sx);
|
|
230
|
+
const y0 = Math.floor(sy);
|
|
231
|
+
const x1 = Math.min(x0 + 1, sw - 1);
|
|
232
|
+
const y1 = Math.min(y0 + 1, sh - 1);
|
|
233
|
+
const fx = sx - x0;
|
|
234
|
+
const fy = sy - y0;
|
|
235
|
+
const w00 = (1 - fx) * (1 - fy);
|
|
236
|
+
const w10 = fx * (1 - fy);
|
|
237
|
+
const w01 = (1 - fx) * fy;
|
|
238
|
+
const w11 = fx * fy;
|
|
239
|
+
const i00 = (y0 * sw + x0) * 4;
|
|
240
|
+
const i10 = (y0 * sw + x1) * 4;
|
|
241
|
+
const i01 = (y1 * sw + x0) * 4;
|
|
242
|
+
const i11 = (y1 * sw + x1) * 4;
|
|
243
|
+
dstData[di] =
|
|
244
|
+
srcData[i00] * w00 + srcData[i10] * w10 + srcData[i01] * w01 + srcData[i11] * w11;
|
|
245
|
+
dstData[di + 1] =
|
|
246
|
+
srcData[i00 + 1] * w00 +
|
|
247
|
+
srcData[i10 + 1] * w10 +
|
|
248
|
+
srcData[i01 + 1] * w01 +
|
|
249
|
+
srcData[i11 + 1] * w11;
|
|
250
|
+
dstData[di + 2] =
|
|
251
|
+
srcData[i00 + 2] * w00 +
|
|
252
|
+
srcData[i10 + 2] * w10 +
|
|
253
|
+
srcData[i01 + 2] * w01 +
|
|
254
|
+
srcData[i11 + 2] * w11;
|
|
255
|
+
dstData[di + 3] =
|
|
256
|
+
srcData[i00 + 3] * w00 +
|
|
257
|
+
srcData[i10 + 3] * w10 +
|
|
258
|
+
srcData[i01 + 3] * w01 +
|
|
259
|
+
srcData[i11 + 3] * w11;
|
|
260
|
+
} else {
|
|
261
|
+
const sxi = sx | 0;
|
|
262
|
+
const syi = sy | 0;
|
|
263
|
+
const si = (syi * sw + sxi) * 4;
|
|
264
|
+
dstData[di] = srcData[si];
|
|
265
|
+
dstData[di + 1] = srcData[si + 1];
|
|
266
|
+
dstData[di + 2] = srcData[si + 2];
|
|
267
|
+
dstData[di + 3] = srcData[si + 3];
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
bctx.putImageData(dstImage, 0, 0);
|
|
273
|
+
|
|
274
|
+
// Clip to the projection's sphere so pixels outside the visible region
|
|
275
|
+
// (e.g. the far hemisphere of an orthographic globe) aren't drawn. For
|
|
276
|
+
// non-clipped projections (mercator, etc.) the sphere still renders as a
|
|
277
|
+
// finite region covering the full map.
|
|
278
|
+
dstCtx.save();
|
|
279
|
+
const pathGen = geoPath(projection, dstCtx);
|
|
280
|
+
dstCtx.beginPath();
|
|
281
|
+
pathGen({ type: 'Sphere' });
|
|
282
|
+
dstCtx.clip();
|
|
283
|
+
|
|
284
|
+
const prevSmoothing = dstCtx.imageSmoothingEnabled;
|
|
285
|
+
if (pixelated) dstCtx.imageSmoothingEnabled = false;
|
|
286
|
+
dstCtx.drawImage(buffer, 0, 0, ctx.width, ctx.height);
|
|
287
|
+
if (pixelated) dstCtx.imageSmoothingEnabled = prevSmoothing;
|
|
288
|
+
|
|
289
|
+
dstCtx.restore();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (layerCtx === 'canvas') {
|
|
293
|
+
ctx.registerComponent({
|
|
294
|
+
name: 'GeoRaster',
|
|
295
|
+
kind: 'mark',
|
|
296
|
+
canvasRender: {
|
|
297
|
+
render,
|
|
298
|
+
deps: () => [
|
|
299
|
+
source,
|
|
300
|
+
geo.projection,
|
|
301
|
+
ctx.width,
|
|
302
|
+
ctx.height,
|
|
303
|
+
interpolate,
|
|
304
|
+
resolution,
|
|
305
|
+
pixelated,
|
|
306
|
+
sourceProjection,
|
|
307
|
+
],
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
</script>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { GeoProjection } from 'd3-geo';
|
|
2
|
+
export type GeoRasterPropsWithoutHTML = {
|
|
3
|
+
/**
|
|
4
|
+
* The image to reproject. Either:
|
|
5
|
+
*
|
|
6
|
+
* - a URL string (loaded via `Image` with `crossOrigin="anonymous"`),
|
|
7
|
+
* - a preloaded `HTMLImageElement`,
|
|
8
|
+
* - an `HTMLCanvasElement` (e.g. a stitched tile mosaic),
|
|
9
|
+
* - or an `ImageBitmap`.
|
|
10
|
+
*
|
|
11
|
+
* The image pixel data must be readable (not tainted by CORS). When passing a
|
|
12
|
+
* URL, the component sets `crossOrigin` on the underlying `Image` — the host
|
|
13
|
+
* must respond with the appropriate `Access-Control-Allow-Origin` header.
|
|
14
|
+
*/
|
|
15
|
+
image: string | HTMLImageElement | HTMLCanvasElement | ImageBitmap;
|
|
16
|
+
/**
|
|
17
|
+
* Projection used by the source image. Pass as an uncalled function, e.g.
|
|
18
|
+
* `sourceProjection={geoEquirectangular}`.
|
|
19
|
+
*
|
|
20
|
+
* When omitted, the source is assumed to be an equirectangular (plate carrée)
|
|
21
|
+
* image covering the full globe — i.e. `x = ((lon + 180) / 360) * width`,
|
|
22
|
+
* `y = ((90 - lat) / 180) * height`. This is the common layout for imagery
|
|
23
|
+
* such as NASA Blue Marble / Black Marble.
|
|
24
|
+
*/
|
|
25
|
+
sourceProjection?: () => GeoProjection;
|
|
26
|
+
/**
|
|
27
|
+
* Pixel sampling strategy.
|
|
28
|
+
*
|
|
29
|
+
* - `nearest` (default): fastest, slightly blocky at low source resolution.
|
|
30
|
+
* - `bilinear`: smoother, ~4x slower per pixel.
|
|
31
|
+
*
|
|
32
|
+
* @default 'nearest'
|
|
33
|
+
*/
|
|
34
|
+
interpolate?: 'nearest' | 'bilinear';
|
|
35
|
+
/**
|
|
36
|
+
* `crossOrigin` attribute applied to the underlying `Image` element when
|
|
37
|
+
* loading from a URL. Set to `null` to disable.
|
|
38
|
+
*
|
|
39
|
+
* @default 'anonymous'
|
|
40
|
+
*/
|
|
41
|
+
crossOrigin?: string | null;
|
|
42
|
+
/**
|
|
43
|
+
* Resolution multiplier for the reprojected output, relative to chart CSS
|
|
44
|
+
* pixels. A value greater than 1 produces a sharper image on HiDPI
|
|
45
|
+
* displays at the cost of more work per frame.
|
|
46
|
+
*
|
|
47
|
+
* @default 1
|
|
48
|
+
*/
|
|
49
|
+
resolution?: number;
|
|
50
|
+
/**
|
|
51
|
+
* Disable image smoothing when compositing the reprojected buffer onto the
|
|
52
|
+
* destination canvas. Useful for a pixel-art look.
|
|
53
|
+
*
|
|
54
|
+
* @default false
|
|
55
|
+
*/
|
|
56
|
+
pixelated?: boolean;
|
|
57
|
+
};
|
|
58
|
+
export type GeoRasterProps = GeoRasterPropsWithoutHTML;
|
|
59
|
+
declare const GeoRaster: import("svelte").Component<GeoRasterPropsWithoutHTML, {}, "">;
|
|
60
|
+
type GeoRaster = ReturnType<typeof GeoRaster>;
|
|
61
|
+
export default GeoRaster;
|
|
@@ -44,6 +44,12 @@
|
|
|
44
44
|
*/
|
|
45
45
|
radialY?: 'circle' | 'linear';
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Stroke color for grid lines.
|
|
49
|
+
* Useful for server-side rendering where CSS variables are not available.
|
|
50
|
+
*/
|
|
51
|
+
stroke?: string;
|
|
52
|
+
|
|
47
53
|
/**
|
|
48
54
|
* Classes to apply to the rendered elements.
|
|
49
55
|
*
|
|
@@ -113,6 +119,7 @@
|
|
|
113
119
|
yTicks: yTicksProp,
|
|
114
120
|
bandAlign = 'center',
|
|
115
121
|
radialY = 'circle',
|
|
122
|
+
stroke,
|
|
116
123
|
motion,
|
|
117
124
|
transitionIn: transitionInProp,
|
|
118
125
|
transitionInParams = { easing: cubicIn },
|
|
@@ -168,6 +175,7 @@
|
|
|
168
175
|
{y1}
|
|
169
176
|
{x2}
|
|
170
177
|
{y2}
|
|
178
|
+
{stroke}
|
|
171
179
|
motion={tweenConfig}
|
|
172
180
|
{...splineProps}
|
|
173
181
|
class={cls('lc-grid-x-radial-line', classes.line, splineProps?.class)}
|
|
@@ -176,6 +184,7 @@
|
|
|
176
184
|
<Rule
|
|
177
185
|
{x}
|
|
178
186
|
xOffset={xBandOffset}
|
|
187
|
+
{stroke}
|
|
179
188
|
{motion}
|
|
180
189
|
{...splineProps}
|
|
181
190
|
class={cls('lc-grid-x-rule', classes.line, splineProps?.class)}
|
|
@@ -189,6 +198,7 @@
|
|
|
189
198
|
<Rule
|
|
190
199
|
x={xTickVals[xTickVals.length - 1]}
|
|
191
200
|
xOffset={ctx.xScale.step() + xBandOffset}
|
|
201
|
+
{stroke}
|
|
192
202
|
{motion}
|
|
193
203
|
{...splineProps}
|
|
194
204
|
class={cls('lc-grid-x-end-rule', classes.line, splineProps?.class)}
|
|
@@ -205,6 +215,7 @@
|
|
|
205
215
|
{#if radialY === 'circle'}
|
|
206
216
|
<Circle
|
|
207
217
|
r={ctx.yScale(y) + yBandOffset}
|
|
218
|
+
{stroke}
|
|
208
219
|
{motion}
|
|
209
220
|
{...splineProps}
|
|
210
221
|
class={cls('lc-grid-y-radial-circle', classes.line, splineProps?.class)}
|
|
@@ -214,6 +225,7 @@
|
|
|
214
225
|
data={xTickVals.map((x) => ({ x, y }))}
|
|
215
226
|
x="x"
|
|
216
227
|
y="y"
|
|
228
|
+
{stroke}
|
|
217
229
|
motion={tweenConfig}
|
|
218
230
|
curve={curveLinearClosed}
|
|
219
231
|
{...splineProps}
|
|
@@ -226,6 +238,7 @@
|
|
|
226
238
|
y1={ctx.yScale(y) + yBandOffset}
|
|
227
239
|
x2={ctx.xRange[1]}
|
|
228
240
|
y2={ctx.yScale(y) + yBandOffset}
|
|
241
|
+
{stroke}
|
|
229
242
|
{motion}
|
|
230
243
|
{...splineProps}
|
|
231
244
|
class={cls('lc-grid-y-rule', classes.line, splineProps?.class)}
|
|
@@ -238,6 +251,7 @@
|
|
|
238
251
|
{#if ctx.radial}
|
|
239
252
|
<Circle
|
|
240
253
|
r={ctx.yScale(yTickVals[yTickVals.length - 1])! + ctx.yScale.step() + yBandOffset}
|
|
254
|
+
{stroke}
|
|
241
255
|
{motion}
|
|
242
256
|
{...splineProps}
|
|
243
257
|
class={cls('lc-grid-y-radial-circle', classes.line, splineProps?.class)}
|
|
@@ -250,6 +264,7 @@
|
|
|
250
264
|
y1={y}
|
|
251
265
|
x2={ctx.xRange[1]}
|
|
252
266
|
y2={y}
|
|
267
|
+
{stroke}
|
|
253
268
|
{motion}
|
|
254
269
|
{...splineProps}
|
|
255
270
|
class={cls('lc-grid-y-end-rule', classes.line, splineProps?.class)}
|
|
@@ -36,6 +36,11 @@ export type GridPropsWithoutHTML<In extends Transition = Transition> = {
|
|
|
36
36
|
* @default 'circle'
|
|
37
37
|
*/
|
|
38
38
|
radialY?: 'circle' | 'linear';
|
|
39
|
+
/**
|
|
40
|
+
* Stroke color for grid lines.
|
|
41
|
+
* Useful for server-side rendering where CSS variables are not available.
|
|
42
|
+
*/
|
|
43
|
+
stroke?: string;
|
|
39
44
|
/**
|
|
40
45
|
* Classes to apply to the rendered elements.
|
|
41
46
|
*
|
|
@@ -514,7 +514,7 @@
|
|
|
514
514
|
style:object-fit="cover"
|
|
515
515
|
crossorigin={crossOrigin}
|
|
516
516
|
class={cls('lc-image', className)}
|
|
517
|
-
{...restProps}
|
|
517
|
+
{...restProps as any}
|
|
518
518
|
/>
|
|
519
519
|
{/each}
|
|
520
520
|
{:else}
|
|
@@ -532,7 +532,7 @@
|
|
|
532
532
|
style:object-fit="cover"
|
|
533
533
|
crossorigin={crossOrigin}
|
|
534
534
|
class={cls('lc-image', className)}
|
|
535
|
-
{...restProps}
|
|
535
|
+
{...restProps as any}
|
|
536
536
|
/>
|
|
537
537
|
{/if}
|
|
538
538
|
{/if}
|
|
@@ -125,7 +125,13 @@
|
|
|
125
125
|
import { getLayerContext } from '../contexts/layer.js';
|
|
126
126
|
import { getChartContext } from '../contexts/chart.js';
|
|
127
127
|
import { createDataMotionMap } from '../utils/motion.svelte.js';
|
|
128
|
-
import {
|
|
128
|
+
import {
|
|
129
|
+
hasAnyDataProp,
|
|
130
|
+
resolveDataProp,
|
|
131
|
+
resolveColorProp,
|
|
132
|
+
resolveGeoDataPair,
|
|
133
|
+
resolveStyleProp,
|
|
134
|
+
} from '../utils/dataProp.js';
|
|
129
135
|
import { getGeoContext } from '../contexts/geo.js';
|
|
130
136
|
import { chartDataArray } from '../utils/common.js';
|
|
131
137
|
|
|
@@ -167,9 +173,7 @@
|
|
|
167
173
|
const geo = getGeoContext();
|
|
168
174
|
|
|
169
175
|
// Data to iterate over in data mode
|
|
170
|
-
const resolvedData: any[] = $derived(
|
|
171
|
-
dataMode ? (dataProp ?? chartDataArray(chartCtx.data)) : []
|
|
172
|
-
);
|
|
176
|
+
const resolvedData: any[] = $derived(dataMode ? (dataProp ?? chartDataArray(chartCtx.data)) : []);
|
|
173
177
|
|
|
174
178
|
// Resolve a single data item to pixel coordinates
|
|
175
179
|
function resolveLine(d: any) {
|
|
@@ -233,29 +237,21 @@
|
|
|
233
237
|
const _initialX2 = initialX2 ?? (typeof x2 === 'number' ? x2 : 0);
|
|
234
238
|
const _initialY2 = initialY2 ?? (typeof y2 === 'number' ? y2 : 0);
|
|
235
239
|
|
|
236
|
-
const motionX1 = createMotion(
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
);
|
|
241
|
-
const motionY1 = createMotion(
|
|
242
|
-
_initialY1,
|
|
243
|
-
() => (typeof y1 === 'number' ? y1 : 0),
|
|
244
|
-
motion
|
|
245
|
-
);
|
|
246
|
-
const motionX2 = createMotion(
|
|
247
|
-
_initialX2,
|
|
248
|
-
() => (typeof x2 === 'number' ? x2 : 0),
|
|
249
|
-
motion
|
|
250
|
-
);
|
|
251
|
-
const motionY2 = createMotion(
|
|
252
|
-
_initialY2,
|
|
253
|
-
() => (typeof y2 === 'number' ? y2 : 0),
|
|
254
|
-
motion
|
|
255
|
-
);
|
|
240
|
+
const motionX1 = createMotion(_initialX1, () => (typeof x1 === 'number' ? x1 : 0), motion);
|
|
241
|
+
const motionY1 = createMotion(_initialY1, () => (typeof y1 === 'number' ? y1 : 0), motion);
|
|
242
|
+
const motionX2 = createMotion(_initialX2, () => (typeof x2 === 'number' ? x2 : 0), motion);
|
|
243
|
+
const motionY2 = createMotion(_initialY2, () => (typeof y2 === 'number' ? y2 : 0), motion);
|
|
256
244
|
|
|
257
245
|
const layerCtx = getLayerContext();
|
|
258
246
|
|
|
247
|
+
const staticFill = $derived(typeof fill === 'string' ? fill : undefined);
|
|
248
|
+
const staticStroke = $derived(typeof stroke === 'string' ? stroke : undefined);
|
|
249
|
+
const staticFillOpacity = $derived(typeof fillOpacity === 'number' ? fillOpacity : undefined);
|
|
250
|
+
const staticStrokeWidth = $derived(typeof strokeWidth === 'number' ? strokeWidth : undefined);
|
|
251
|
+
const staticOpacity = $derived(typeof opacity === 'number' ? opacity : undefined);
|
|
252
|
+
const staticClassName = $derived(typeof className === 'string' ? className : undefined);
|
|
253
|
+
const staticHeight = $derived(typeof strokeWidth === 'number' ? `${strokeWidth}px` : '1px');
|
|
254
|
+
|
|
259
255
|
function getStyleOptions(
|
|
260
256
|
styleOverrides: ComputedStylesOptions | undefined,
|
|
261
257
|
itemFill?: string | undefined,
|
|
@@ -266,16 +262,29 @@
|
|
|
266
262
|
itemClass?: string | undefined
|
|
267
263
|
) {
|
|
268
264
|
return styleOverrides
|
|
269
|
-
? merge(
|
|
265
|
+
? merge(
|
|
266
|
+
{
|
|
267
|
+
styles: {
|
|
268
|
+
strokeWidth:
|
|
269
|
+
itemStrokeWidth ?? (typeof strokeWidth === 'number' ? strokeWidth : undefined),
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
styleOverrides
|
|
273
|
+
)
|
|
270
274
|
: {
|
|
271
275
|
styles: {
|
|
272
276
|
fill: itemFill ?? fill,
|
|
273
|
-
fillOpacity:
|
|
277
|
+
fillOpacity:
|
|
278
|
+
itemFillOpacity ?? (typeof fillOpacity === 'number' ? fillOpacity : undefined),
|
|
274
279
|
stroke: itemStroke ?? stroke,
|
|
275
|
-
strokeWidth:
|
|
280
|
+
strokeWidth:
|
|
281
|
+
itemStrokeWidth ?? (typeof strokeWidth === 'number' ? strokeWidth : undefined),
|
|
276
282
|
opacity: itemOpacity ?? (typeof opacity === 'number' ? opacity : undefined),
|
|
277
283
|
},
|
|
278
|
-
classes: cls(
|
|
284
|
+
classes: cls(
|
|
285
|
+
'lc-line',
|
|
286
|
+
itemClass ?? (typeof className === 'string' ? className : undefined)
|
|
287
|
+
),
|
|
279
288
|
style: restProps.style as string | undefined,
|
|
280
289
|
};
|
|
281
290
|
}
|
|
@@ -292,7 +301,15 @@
|
|
|
292
301
|
const resolvedStrokeWidth = resolveStyleProp(strokeWidth, item.d);
|
|
293
302
|
const resolvedOpacity = resolveStyleProp(opacity, item.d);
|
|
294
303
|
const resolvedClass = resolveStyleProp(className, item.d);
|
|
295
|
-
const styleOpts = getStyleOptions(
|
|
304
|
+
const styleOpts = getStyleOptions(
|
|
305
|
+
styleOverrides,
|
|
306
|
+
resolvedFill,
|
|
307
|
+
resolvedStroke,
|
|
308
|
+
resolvedFillOpacity,
|
|
309
|
+
resolvedStrokeWidth,
|
|
310
|
+
resolvedOpacity,
|
|
311
|
+
resolvedClass
|
|
312
|
+
);
|
|
296
313
|
const pathData = `M ${item.x1},${item.y1} L ${item.x2},${item.y2}`;
|
|
297
314
|
renderPathData(ctx, pathData, styleOpts);
|
|
298
315
|
}
|
|
@@ -318,29 +335,32 @@
|
|
|
318
335
|
color: typeof stroke === 'string' ? stroke : typeof fill === 'string' ? fill : undefined,
|
|
319
336
|
};
|
|
320
337
|
},
|
|
321
|
-
canvasRender:
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
338
|
+
canvasRender:
|
|
339
|
+
layerCtx === 'canvas'
|
|
340
|
+
? {
|
|
341
|
+
render,
|
|
342
|
+
events: {
|
|
343
|
+
click: restProps.onclick,
|
|
344
|
+
pointerenter: restProps.onpointerenter,
|
|
345
|
+
pointermove: restProps.onpointermove,
|
|
346
|
+
pointerleave: restProps.onpointerleave,
|
|
347
|
+
},
|
|
348
|
+
deps: () => [
|
|
349
|
+
dataMode,
|
|
350
|
+
dataMode ? resolvedItems : null,
|
|
351
|
+
motionX1.current,
|
|
352
|
+
motionY1.current,
|
|
353
|
+
motionX2.current,
|
|
354
|
+
motionY2.current,
|
|
355
|
+
fillKey!.current,
|
|
356
|
+
strokeKey!.current,
|
|
357
|
+
strokeWidth,
|
|
358
|
+
opacity,
|
|
359
|
+
className,
|
|
360
|
+
restProps.style,
|
|
361
|
+
],
|
|
362
|
+
}
|
|
363
|
+
: undefined,
|
|
344
364
|
});
|
|
345
365
|
</script>
|
|
346
366
|
|
|
@@ -380,15 +400,15 @@
|
|
|
380
400
|
y1={motionY1.current}
|
|
381
401
|
x2={motionX2.current}
|
|
382
402
|
y2={motionY2.current}
|
|
383
|
-
fill={
|
|
384
|
-
stroke={
|
|
385
|
-
fill-opacity={
|
|
386
|
-
stroke-width={
|
|
387
|
-
opacity={
|
|
403
|
+
fill={staticFill}
|
|
404
|
+
stroke={staticStroke}
|
|
405
|
+
fill-opacity={staticFillOpacity}
|
|
406
|
+
stroke-width={staticStrokeWidth}
|
|
407
|
+
opacity={staticOpacity}
|
|
388
408
|
marker-start={markerStartId ? `url(#${markerStartId})` : undefined}
|
|
389
409
|
marker-mid={markerMidId ? `url(#${markerMidId})` : undefined}
|
|
390
410
|
marker-end={markerEndId ? `url(#${markerEndId})` : undefined}
|
|
391
|
-
class={cls('lc-line',
|
|
411
|
+
class={cls('lc-line', staticClassName)}
|
|
392
412
|
{...restProps}
|
|
393
413
|
/>
|
|
394
414
|
<MarkerWrapper id={markerStartId} marker={markerStart ?? marker} />
|
|
@@ -431,12 +451,12 @@
|
|
|
431
451
|
style:left="{motionX1.current}px"
|
|
432
452
|
style:top="{motionY1.current}px"
|
|
433
453
|
style:width="{length}px"
|
|
434
|
-
style:height=
|
|
454
|
+
style:height={staticHeight}
|
|
435
455
|
style:transform="translateY(-50%) rotate({angle}deg)"
|
|
436
456
|
style:transform-origin="0 50%"
|
|
437
|
-
style:opacity={
|
|
438
|
-
style:background-color={
|
|
439
|
-
class={cls('lc-line',
|
|
457
|
+
style:opacity={staticOpacity}
|
|
458
|
+
style:background-color={staticStroke}
|
|
459
|
+
class={cls('lc-line', staticClassName)}
|
|
440
460
|
style={restProps.style}
|
|
441
461
|
></div>
|
|
442
462
|
{/if}
|