layerchart 2.0.0-next.47 → 2.0.0-next.49

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.
Files changed (67) hide show
  1. package/dist/bench/PrimitiveBench.svelte +66 -0
  2. package/dist/bench/PrimitiveBench.svelte.d.ts +10 -0
  3. package/dist/bench/primitives.svelte.bench.d.ts +1 -0
  4. package/dist/bench/primitives.svelte.bench.js +42 -0
  5. package/dist/components/Axis.svelte +14 -3
  6. package/dist/components/Axis.svelte.d.ts +1 -1
  7. package/dist/components/Chart.svelte +110 -12
  8. package/dist/components/Circle.svelte +20 -17
  9. package/dist/components/Contour.svelte +90 -13
  10. package/dist/components/Contour.svelte.d.ts +8 -0
  11. package/dist/components/Ellipse.svelte +18 -16
  12. package/dist/components/GeoPath.svelte +1 -1
  13. package/dist/components/Group.svelte +14 -12
  14. package/dist/components/Image.svelte +18 -16
  15. package/dist/components/Labels.svelte +56 -11
  16. package/dist/components/Labels.svelte.d.ts +3 -2
  17. package/dist/components/Line.svelte +18 -16
  18. package/dist/components/LinearGradient.svelte +1 -1
  19. package/dist/components/Marker.svelte +8 -3
  20. package/dist/components/Marker.svelte.d.ts +1 -1
  21. package/dist/components/Month.svelte +273 -0
  22. package/dist/components/Month.svelte.d.ts +70 -0
  23. package/dist/components/Path.svelte +28 -12
  24. package/dist/components/Polygon.svelte +25 -23
  25. package/dist/components/RadialGradient.svelte +1 -1
  26. package/dist/components/Raster.svelte +117 -29
  27. package/dist/components/Raster.svelte.d.ts +8 -0
  28. package/dist/components/Rect.svelte +26 -20
  29. package/dist/components/Spline.svelte +123 -25
  30. package/dist/components/Spline.svelte.d.ts +18 -1
  31. package/dist/components/Text.svelte +45 -20
  32. package/dist/components/Text.svelte.d.ts +6 -0
  33. package/dist/components/TransformContext.svelte +8 -0
  34. package/dist/components/TransformContext.svelte.test.d.ts +1 -0
  35. package/dist/components/TransformContext.svelte.test.js +166 -0
  36. package/dist/components/Vector.svelte +14 -12
  37. package/dist/components/index.d.ts +2 -0
  38. package/dist/components/index.js +2 -0
  39. package/dist/components/tests/TransformTestHarness.svelte +27 -0
  40. package/dist/components/tests/TransformTestHarness.svelte.d.ts +8 -0
  41. package/dist/states/__fixtures__/ComponentNodeLifecycleChild.svelte +1 -1
  42. package/dist/states/__screenshots__/chart.component-node.svelte.test.ts/ChartState-registerComponent-cleans-up-child-nodes-and-mark-registrations-when-components-unmount-1.png +0 -0
  43. package/dist/states/__screenshots__/chart.component-node.svelte.test.ts/ChartState-registerComponent-cleans-up-child-nodes-and-mark-registrations-when-components-unmount-2.png +0 -0
  44. package/dist/states/brush.svelte.d.ts +26 -17
  45. package/dist/states/brush.svelte.js +118 -25
  46. package/dist/states/brush.svelte.test.js +126 -1
  47. package/dist/states/chart.svelte.d.ts +6 -0
  48. package/dist/states/chart.svelte.js +100 -21
  49. package/dist/states/chart.svelte.test.js +16 -1
  50. package/dist/states/transform.svelte.js +3 -1
  51. package/dist/utils/dataProp.d.ts +2 -10
  52. package/dist/utils/dataProp.js +16 -5
  53. package/dist/utils/index.d.ts +1 -0
  54. package/dist/utils/index.js +1 -0
  55. package/dist/utils/motion.svelte.d.ts +12 -2
  56. package/dist/utils/motion.svelte.js +22 -0
  57. package/dist/utils/motion.test.js +49 -1
  58. package/dist/utils/rasterBounds.d.ts +18 -0
  59. package/dist/utils/rasterBounds.js +98 -0
  60. package/dist/utils/rasterBounds.test.d.ts +1 -0
  61. package/dist/utils/rasterBounds.test.js +63 -0
  62. package/dist/utils/scales.svelte.js +4 -2
  63. package/dist/utils/scales.svelte.test.d.ts +1 -0
  64. package/dist/utils/scales.svelte.test.js +67 -0
  65. package/dist/utils/ticks.js +7 -3
  66. package/dist/utils/ticks.test.js +13 -3
  67. package/package.json +3 -2
@@ -0,0 +1,70 @@
1
+ export type MonthCell = {
2
+ x: number;
3
+ y: number;
4
+ color: any;
5
+ data: any;
6
+ date: Date;
7
+ };
8
+ export type MonthPropsWithoutHTML = {
9
+ /**
10
+ * The start date of the calendar.
11
+ */
12
+ start: Date;
13
+ /**
14
+ * The end date of the calendar.
15
+ */
16
+ end: Date;
17
+ /**
18
+ * Size of the cell in the calendar.
19
+ *
20
+ * @default 25
21
+ */
22
+ cellSize?: number;
23
+ /**
24
+ * Number of months to display per row. If undefined, automatically calculated based on available width.
25
+ */
26
+ monthsPerRow?: number;
27
+ /**
28
+ * Padding multiplier between months (relative to cellSize).
29
+ *
30
+ * @default 1.2
31
+ */
32
+ monthPadding?: number;
33
+ /**
34
+ * Vertical spacing multiplier between month rows (in number of cell heights).
35
+ *
36
+ * @default 8
37
+ */
38
+ rowSpacing?: number;
39
+ /**
40
+ * Whether to show the day number in each cell.
41
+ *
42
+ * @default true
43
+ */
44
+ showDayNumber?: boolean;
45
+ /**
46
+ * Props to pass to the `<text>` element for month labels.
47
+ */
48
+ monthLabel?: boolean | Partial<ComponentProps<typeof Text>>;
49
+ /**
50
+ * Props to pass to the `<text>` element for day numbers.
51
+ */
52
+ dayNumberProps?: Partial<ComponentProps<typeof Text>>;
53
+ /**
54
+ * Setup pointer events to show tooltip for related data
55
+ */
56
+ tooltip?: boolean;
57
+ children?: Snippet<[{
58
+ cells: MonthCell[];
59
+ cellSize: number;
60
+ }]>;
61
+ } & Omit<RectPropsWithoutHTML, 'children' | 'x' | 'y' | 'width' | 'height' | 'fill' | 'onpointermove' | 'onpointerleave'>;
62
+ export type MonthProps = MonthPropsWithoutHTML & Without<SVGAttributes<SVGRectElement>, MonthPropsWithoutHTML>;
63
+ import { type ComponentProps, type Snippet } from 'svelte';
64
+ import { type RectPropsWithoutHTML } from './Rect.svelte';
65
+ import Text from './Text.svelte';
66
+ import type { SVGAttributes } from 'svelte/elements';
67
+ import type { Without } from '../utils/types.js';
68
+ declare const Month: import("svelte").Component<MonthPropsWithoutHTML, {}, "">;
69
+ type Month = ReturnType<typeof Month>;
70
+ export default Month;
@@ -118,6 +118,14 @@
118
118
  endContent,
119
119
  opacity,
120
120
  pathRef: pathRefProp = $bindable(),
121
+ onclick,
122
+ onpointerenter,
123
+ onpointermove,
124
+ onpointerleave,
125
+ onpointerdown,
126
+ onpointerover,
127
+ onpointerout,
128
+ ontouchmove,
121
129
  ...restProps
122
130
  }: PathProps = $props();
123
131
 
@@ -185,26 +193,26 @@
185
193
  }
186
194
 
187
195
  // TODO: Use objectId to work around Svelte 4 reactivity issue (even when memoizing gradients)
188
- const fillKey = createKey(() => fill);
189
- const strokeKey = createKey(() => stroke);
196
+ const fillKey = layerCtx === 'canvas' ? createKey(() => fill) : undefined;
197
+ const strokeKey = layerCtx === 'canvas' ? createKey(() => stroke) : undefined;
190
198
 
191
199
  if (layerCtx === 'canvas') {
192
200
  ctx.registerComponent({ name: 'Path', kind: 'mark', canvasRender: {
193
201
  render,
194
202
  events: {
195
- click: restProps.onclick,
196
- pointerenter: restProps.onpointerenter,
197
- pointermove: restProps.onpointermove,
198
- pointerleave: restProps.onpointerleave,
199
- pointerdown: restProps.onpointerdown,
200
- pointerover: restProps.onpointerover,
201
- pointerout: restProps.onpointerout,
202
- touchmove: restProps.ontouchmove,
203
+ get click() { return onclick; },
204
+ get pointerenter() { return onpointerenter; },
205
+ get pointermove() { return onpointermove; },
206
+ get pointerleave() { return onpointerleave; },
207
+ get pointerdown() { return onpointerdown; },
208
+ get pointerover() { return onpointerover; },
209
+ get pointerout() { return onpointerout; },
210
+ get touchmove() { return ontouchmove; },
203
211
  },
204
212
  deps: () => [
205
- fillKey.current,
213
+ fillKey!.current,
206
214
  fillOpacity,
207
- strokeKey.current,
215
+ strokeKey!.current,
208
216
  strokeOpacity,
209
217
  strokeWidth,
210
218
  opacity,
@@ -275,6 +283,14 @@
275
283
  <path
276
284
  d={tweenedState.current}
277
285
  {...restProps}
286
+ {onclick}
287
+ {onpointerenter}
288
+ {onpointermove}
289
+ {onpointerleave}
290
+ {onpointerdown}
291
+ {onpointerover}
292
+ {onpointerout}
293
+ {ontouchmove}
278
294
  class={cls('lc-path', className)}
279
295
  {fill}
280
296
  fill-opacity={fillOpacity}
@@ -257,26 +257,28 @@
257
257
  // --- Data mode motion ---
258
258
  const dataMotionMap = createDataMotionMap(motion);
259
259
 
260
- $effect(() => {
261
- if (!dataMode || !dataMotionMap) return;
262
- const activeKeys = new Set<any>();
263
- for (let i = 0; i < resolvedData.length; i++) {
264
- const d = resolvedData[i];
265
- const key = keyFn(d, i);
266
- activeKeys.add(key);
267
- // Polygon resolve returns a path string, so resolve coords separately for motion
268
- let resolvedCx: number, resolvedCy: number;
269
- if (geo.projection) {
270
- [resolvedCx, resolvedCy] = resolveGeoDataPair(cx, cy, d, geo.projection);
271
- } else {
272
- resolvedCx = resolveDataProp(cx, d, chartCtx.xScale, 0);
273
- resolvedCy = resolveDataProp(cy, d, chartCtx.yScale, 0);
260
+ if (dataMotionMap) {
261
+ $effect(() => {
262
+ if (!dataMode) return;
263
+ const activeKeys = new Set<any>();
264
+ for (let i = 0; i < resolvedData.length; i++) {
265
+ const d = resolvedData[i];
266
+ const key = keyFn(d, i);
267
+ activeKeys.add(key);
268
+ // Polygon resolve returns a path string, so resolve coords separately for motion
269
+ let resolvedCx: number, resolvedCy: number;
270
+ if (geo.projection) {
271
+ [resolvedCx, resolvedCy] = resolveGeoDataPair(cx, cy, d, geo.projection);
272
+ } else {
273
+ resolvedCx = resolveDataProp(cx, d, chartCtx.xScale, 0);
274
+ resolvedCy = resolveDataProp(cy, d, chartCtx.yScale, 0);
275
+ }
276
+ const resolvedR = resolveDataProp(r, d, chartCtx.rScale, typeof r === 'number' ? r : 1);
277
+ untrack(() => dataMotionMap.update(key, { cx: resolvedCx, cy: resolvedCy, r: resolvedR }));
274
278
  }
275
- const resolvedR = resolveDataProp(r, d, chartCtx.rScale, typeof r === 'number' ? r : 1);
276
- untrack(() => dataMotionMap.update(key, { cx: resolvedCx, cy: resolvedCy, r: resolvedR }));
277
- }
278
- untrack(() => dataMotionMap.cleanup(activeKeys));
279
- });
279
+ untrack(() => dataMotionMap.cleanup(activeKeys));
280
+ });
281
+ }
280
282
 
281
283
  // TODO: Apply animated values from dataMotionMap to SVG/HTML/Canvas templates.
282
284
  // Polygon uses a path string from resolvePolygon(), so animated cx/cy/r values
@@ -395,8 +397,8 @@
395
397
  }
396
398
 
397
399
  // TODO: Use objectId to work around Svelte 4 reactivity issue (even when memoizing gradients)
398
- const fillKey = createKey(() => fill);
399
- const strokeKey = createKey(() => stroke);
400
+ const fillKey = layerCtx === 'canvas' ? createKey(() => fill) : undefined;
401
+ const strokeKey = layerCtx === 'canvas' ? createKey(() => stroke) : undefined;
400
402
 
401
403
  chartCtx.registerComponent({
402
404
  name: 'Polygon',
@@ -425,9 +427,9 @@
425
427
  deps: () => [
426
428
  dataMode,
427
429
  dataMode ? resolvedItems : null,
428
- fillKey.current,
430
+ fillKey!.current,
429
431
  fillOpacity,
430
- strokeKey.current,
432
+ strokeKey!.current,
431
433
  strokeWidth,
432
434
  opacity,
433
435
  className,
@@ -144,7 +144,7 @@
144
144
  }
145
145
 
146
146
  if (layerCtx === 'canvas') {
147
- ctx.registerComponent({ name: 'Gradient', kind: 'mark', canvasRender: {
147
+ ctx.registerComponent({ name: 'Gradient', kind: 'group', canvasRender: {
148
148
  render,
149
149
  deps: () => [stops, cx, cy, fx, fy, ctx.width, ctx.height],
150
150
  } });
@@ -17,6 +17,15 @@
17
17
  /** Grid height (rows). When set, `data` is treated as a flat grid array. */
18
18
  height?: number;
19
19
 
20
+ /** Left bound of the raster in data coordinates. Defaults to `0` in grid mode. */
21
+ x1?: number;
22
+ /** Top bound of the raster in data coordinates. Defaults to `0` in grid mode. */
23
+ y1?: number;
24
+ /** Right bound of the raster in data coordinates. Defaults to `width` in grid mode. */
25
+ x2?: number;
26
+ /** Bottom bound of the raster in data coordinates. Defaults to `height` in grid mode. */
27
+ y2?: number;
28
+
20
29
  /**
21
30
  * Value channel. Interpretation depends on the type:
22
31
  * - `(x, y) => number`: continuous function evaluated at each pixel (function sampling mode)
@@ -50,20 +59,27 @@
50
59
  import { cls } from '@layerstack/tailwind';
51
60
  import { scaleSequential } from 'd3-scale';
52
61
  import { interpolateYlGnBu } from 'd3-scale-chromatic';
53
- import { max, min, blur2 } from 'd3-array';
62
+ import { max, min } from 'd3-array';
54
63
  import { rgb } from 'd3-color';
55
64
 
56
65
  import { accessor as resolveAccessor, chartDataArray } from '../utils/common.js';
57
66
  import { getChartContext } from '../contexts/chart.js';
67
+ import { getGeoContext } from '../contexts/geo.js';
68
+ import { gridCellCenterToBounds, resolveRasterBounds } from '../utils/index.js';
58
69
  import { interpolateGrid } from '../utils/rasterInterpolate.js';
59
70
  import Image from './Image.svelte';
60
71
 
61
72
  const ctx = getChartContext();
73
+ const geo = getGeoContext();
62
74
 
63
75
  let {
64
76
  data: dataProp,
65
77
  width: widthProp,
66
78
  height: heightProp,
79
+ x1: x1Prop,
80
+ y1: y1Prop,
81
+ x2: x2Prop,
82
+ y2: y2Prop,
67
83
  value: valueProp,
68
84
  x: xProp,
69
85
  y: yProp,
@@ -78,6 +94,15 @@
78
94
 
79
95
  // Detect grid mode: data + width/height
80
96
  const isGridMode = $derived(!!(dataProp && widthProp && heightProp));
97
+ const hasExplicitBounds = $derived(
98
+ x1Prop !== undefined || y1Prop !== undefined || x2Prop !== undefined || y2Prop !== undefined
99
+ );
100
+ const gridBounds = $derived(
101
+ resolveRasterBounds(widthProp ?? 0, heightProp ?? 0, x1Prop, y1Prop, x2Prop, y2Prop)
102
+ );
103
+ const useProjectedGridSampling = $derived(
104
+ !!(geo.projection && geo.projection.invert && isGridMode && hasExplicitBounds)
105
+ );
81
106
 
82
107
  // Register as composite-mark with markInfo for grid mode domain participation
83
108
  ctx.registerComponent({
@@ -87,8 +112,8 @@
87
112
  if (!isGridMode) return {};
88
113
  return {
89
114
  data: [
90
- { x: 0, y: 0 },
91
- { x: widthProp, y: heightProp },
115
+ { x: gridBounds.x1, y: gridBounds.y1 },
116
+ { x: gridBounds.x2, y: gridBounds.y2 },
92
117
  ],
93
118
  x: 'x',
94
119
  y: 'y',
@@ -97,26 +122,23 @@
97
122
  });
98
123
 
99
124
  // Grid dimensions (accounting for pixelSize downsampling)
100
- const gridW = $derived(
101
- widthProp ?? Math.max(1, Math.ceil(ctx.width / pixelSize))
102
- );
103
- const gridH = $derived(
104
- heightProp ?? Math.max(1, Math.ceil(ctx.height / pixelSize))
105
- );
125
+ const gridW = $derived(widthProp ?? Math.max(1, Math.ceil(ctx.width / pixelSize)));
126
+ const gridH = $derived(heightProp ?? Math.max(1, Math.ceil(ctx.height / pixelSize)));
106
127
 
107
128
  // Scale factors from grid to chart pixel coordinates
108
129
  const scaleX = $derived(ctx.width / gridW);
109
130
  const scaleY = $derived(ctx.height / gridH);
110
131
 
132
+ const rasterScaleX = $derived(gridW / ctx.width);
133
+ const rasterScaleY = $derived(gridH / ctx.height);
134
+
111
135
  // Resolve grid values
112
- const gridValues = $derived.by(() => {
136
+ const sourceGridValues = $derived.by(() => {
113
137
  if (!ctx.width || !ctx.height) return new Float64Array(0);
114
138
 
115
139
  // Mode 1: Grid data (flat array + width/height)
116
140
  if (isGridMode) {
117
- return dataProp instanceof Float64Array
118
- ? dataProp
119
- : Float64Array.from(dataProp as number[]);
141
+ return dataProp instanceof Float64Array ? dataProp : Float64Array.from(dataProp as number[]);
120
142
  }
121
143
 
122
144
  // Mode 2: Continuous function
@@ -143,7 +165,7 @@
143
165
 
144
166
  const xAcc = xProp ? resolveAccessor(xProp) : ctx.x;
145
167
  const yAcc = yProp ? resolveAccessor(yProp) : ctx.y;
146
- const valAcc = resolveAccessor(valueProp ?? 'value');
168
+ const valAcc = resolveAccessor((valueProp ?? 'value') as Accessor);
147
169
 
148
170
  const points: [number, number, number][] = chartData.map((d: any) => [
149
171
  ctx.xScale(xAcc(d)) / scaleX,
@@ -154,21 +176,43 @@
154
176
  return interpolateGrid(points, gridW, gridH, interpolateMethod);
155
177
  });
156
178
 
157
- // Apply optional blur
158
- const blurredValues = $derived.by(() => {
159
- if (!blurRadius || gridValues.length === 0) return gridValues;
160
- const copy = new Float64Array(gridValues);
161
- blur2({ data: copy, width: gridW, height: gridH }, blurRadius);
162
- return copy;
179
+ const projectedGridPoints = $derived.by(() => {
180
+ if (!useProjectedGridSampling || !widthProp || !heightProp || !geo.projection) return [];
181
+
182
+ const points: [number, number, number][] = [];
183
+ for (let row = 0; row < heightProp; row++) {
184
+ for (let column = 0; column < widthProp; column++) {
185
+ const value = sourceGridValues[row * widthProp + column];
186
+ if (!Number.isFinite(value)) continue;
187
+
188
+ const point = gridCellCenterToBounds(column, row, widthProp, heightProp, gridBounds);
189
+ const projected = geo.projection([point.x, point.y]);
190
+ if (!projected || !Number.isFinite(projected[0]) || !Number.isFinite(projected[1]))
191
+ continue;
192
+
193
+ points.push([projected[0] * rasterScaleX, projected[1] * rasterScaleY, value]);
194
+ }
195
+ }
196
+
197
+ return points;
198
+ });
199
+
200
+ const rasterValues = $derived.by(() => {
201
+ if (useProjectedGridSampling) {
202
+ return interpolateGrid(projectedGridPoints, gridW, gridH, interpolateMethod);
203
+ }
204
+
205
+ return sourceGridValues;
163
206
  });
164
207
 
165
208
  // Color scale: use chart's cScale (with auto-computed domain) or fall back to default
166
209
  const colorScale = $derived.by(() => {
167
- const validValues = blurredValues.filter((v) => !isNaN(v));
210
+ const validValues = rasterValues.filter((v) => !isNaN(v));
168
211
  const minValue = min(validValues) ?? 0;
169
212
  const maxValue = max(validValues) ?? 1;
170
213
  if (ctx.cScale) {
171
- return ctx.cScale.copy().domain([minValue, maxValue]);
214
+ const scale = ctx.cScale.copy();
215
+ return ctx.props.cDomain ? scale : scale.domain([minValue, maxValue]);
172
216
  }
173
217
  return scaleSequential([minValue, maxValue], interpolateYlGnBu);
174
218
  });
@@ -191,23 +235,56 @@
191
235
  return lut;
192
236
  });
193
237
 
238
+ const imagePlacement = $derived.by(() => {
239
+ if (useProjectedGridSampling) {
240
+ return {
241
+ x: ctx.width / 2,
242
+ y: ctx.height / 2,
243
+ width: ctx.width,
244
+ height: ctx.height,
245
+ };
246
+ }
247
+
248
+ if (isGridMode && hasExplicitBounds) {
249
+ const x1 = ctx.xScale(gridBounds.x1);
250
+ const x2 = ctx.xScale(gridBounds.x2);
251
+ const y1 = ctx.yScale(gridBounds.y1);
252
+ const y2 = ctx.yScale(gridBounds.y2);
253
+
254
+ return {
255
+ x: (x1 + x2) / 2,
256
+ y: (y1 + y2) / 2,
257
+ width: Math.abs(x2 - x1),
258
+ height: Math.abs(y2 - y1),
259
+ };
260
+ }
261
+
262
+ return {
263
+ x: ctx.width / 2,
264
+ y: ctx.height / 2,
265
+ width: ctx.width,
266
+ height: ctx.height,
267
+ };
268
+ });
269
+
194
270
  // Generate image data URL via offscreen canvas
195
271
  const imageDataUrl = $derived.by(() => {
196
272
  if (typeof document === 'undefined') return '';
197
- if (blurredValues.length === 0 || gridW <= 0 || gridH <= 0) return '';
273
+ if (rasterValues.length === 0 || gridW <= 0 || gridH <= 0) return '';
198
274
 
199
275
  const [minValue, maxValue] = colorScale.domain();
200
276
  const range = maxValue - minValue || 1;
201
277
 
202
278
  const canvas = document.createElement('canvas');
279
+
203
280
  canvas.width = gridW;
204
281
  canvas.height = gridH;
205
282
  const canvasCtx = canvas.getContext('2d')!;
206
283
  const imageData = canvasCtx.createImageData(gridW, gridH);
207
284
  const pixels = imageData.data;
208
285
 
209
- for (let i = 0; i < blurredValues.length; i++) {
210
- const v = blurredValues[i];
286
+ for (let i = 0; i < rasterValues.length; i++) {
287
+ const v = rasterValues[i];
211
288
  const offset = i * 4;
212
289
 
213
290
  if (isNaN(v)) {
@@ -228,6 +305,17 @@
228
305
  }
229
306
 
230
307
  canvasCtx.putImageData(imageData, 0, 0);
308
+
309
+ if (blurRadius > 0) {
310
+ const blurredCanvas = document.createElement('canvas');
311
+ blurredCanvas.width = gridW;
312
+ blurredCanvas.height = gridH;
313
+ const blurredCtx = blurredCanvas.getContext('2d')!;
314
+ blurredCtx.filter = `blur(${blurRadius}px)`;
315
+ blurredCtx.drawImage(canvas, 0, 0);
316
+ return blurredCanvas.toDataURL();
317
+ }
318
+
231
319
  return canvas.toDataURL();
232
320
  });
233
321
  </script>
@@ -235,10 +323,10 @@
235
323
  {#if imageDataUrl}
236
324
  <Image
237
325
  href={imageDataUrl}
238
- x={ctx.width / 2}
239
- y={ctx.height / 2}
240
- width={ctx.width}
241
- height={ctx.height}
326
+ x={imagePlacement.x}
327
+ y={imagePlacement.y}
328
+ width={imagePlacement.width}
329
+ height={imagePlacement.height}
242
330
  {imageRendering}
243
331
  {opacity}
244
332
  preserveAspectRatio="none"
@@ -14,6 +14,14 @@ export type RasterPropsWithoutHTML = {
14
14
  width?: number;
15
15
  /** Grid height (rows). When set, `data` is treated as a flat grid array. */
16
16
  height?: number;
17
+ /** Left bound of the raster in data coordinates. Defaults to `0` in grid mode. */
18
+ x1?: number;
19
+ /** Top bound of the raster in data coordinates. Defaults to `0` in grid mode. */
20
+ y1?: number;
21
+ /** Right bound of the raster in data coordinates. Defaults to `width` in grid mode. */
22
+ x2?: number;
23
+ /** Bottom bound of the raster in data coordinates. Defaults to `height` in grid mode. */
24
+ y2?: number;
17
25
  /**
18
26
  * Value channel. Interpretation depends on the type:
19
27
  * - `(x, y) => number`: continuous function evaluated at each pixel (function sampling mode)
@@ -240,18 +240,21 @@
240
240
  // --- Data mode motion ---
241
241
  const dataMotionMap = createDataMotionMap(motion as MotionOptions | undefined);
242
242
 
243
- $effect(() => {
244
- if (!dataMode || !dataMotionMap) return;
245
- const activeKeys = new Set<any>();
246
- for (let i = 0; i < resolvedData.length; i++) {
247
- const d = resolvedData[i];
248
- const key = keyFn(d, i);
249
- activeKeys.add(key);
250
- const resolved = resolveRect(d);
251
- untrack(() => dataMotionMap.update(key, resolved));
252
- }
253
- untrack(() => dataMotionMap.cleanup(activeKeys));
254
- });
243
+ // Only create the data motion tracking effect when motion is actually configured
244
+ if (dataMotionMap) {
245
+ $effect(() => {
246
+ if (!dataMode) return;
247
+ const activeKeys = new Set<any>();
248
+ for (let i = 0; i < resolvedData.length; i++) {
249
+ const d = resolvedData[i];
250
+ const key = keyFn(d, i);
251
+ activeKeys.add(key);
252
+ const resolved = resolveRect(d);
253
+ untrack(() => dataMotionMap.update(key, resolved));
254
+ }
255
+ untrack(() => dataMotionMap.cleanup(activeKeys));
256
+ });
257
+ }
255
258
 
256
259
  // Single source of truth: resolved values with animated overlay
257
260
  const resolvedItems = $derived.by(() => {
@@ -288,25 +291,27 @@
288
291
  const _initialWidth = initialWidth ?? (width ?? 0);
289
292
  const _initialHeight = initialHeight ?? (height ?? 0);
290
293
 
294
+ // Parse motion config once, then pass resolved config to each axis
295
+ // (avoids 4 separate parseMotionProp calls that re-parse the same prop)
291
296
  const motionX = createMotion(
292
297
  _initialX,
293
298
  () => (typeof x === 'number' ? x : 0),
294
- parseMotionProp(motion, 'x')
299
+ motion === undefined ? undefined : parseMotionProp(motion, 'x')
295
300
  );
296
301
  const motionY = createMotion(
297
302
  _initialY,
298
303
  () => (typeof y === 'number' ? y : 0),
299
- parseMotionProp(motion, 'y')
304
+ motion === undefined ? undefined : parseMotionProp(motion, 'y')
300
305
  );
301
306
  const motionWidth = createMotion(
302
307
  _initialWidth,
303
308
  () => width ?? 0,
304
- parseMotionProp(motion, 'width')
309
+ motion === undefined ? undefined : parseMotionProp(motion, 'width')
305
310
  );
306
311
  const motionHeight = createMotion(
307
312
  _initialHeight,
308
313
  () => height ?? 0,
309
- parseMotionProp(motion, 'height')
314
+ motion === undefined ? undefined : parseMotionProp(motion, 'height')
310
315
  );
311
316
 
312
317
  const layerCtx = getLayerContext();
@@ -378,8 +383,9 @@
378
383
  }
379
384
 
380
385
  // TODO: Use objectId to work around Svelte 4 reactivity issue (even when memoizing gradients)
381
- const fillKey = createKey(() => fill);
382
- const strokeKey = createKey(() => stroke);
386
+ // Only create key trackers when in canvas mode (they're only used for canvas dep tracking)
387
+ const fillKey = layerCtx === 'canvas' ? createKey(() => fill) : undefined;
388
+ const strokeKey = layerCtx === 'canvas' ? createKey(() => stroke) : undefined;
383
389
 
384
390
  chartCtx.registerComponent({
385
391
  name: 'Rect',
@@ -411,8 +417,8 @@
411
417
  motionY.current,
412
418
  motionWidth.current,
413
419
  motionHeight.current,
414
- fillKey.current,
415
- strokeKey.current,
420
+ fillKey!.current,
421
+ strokeKey!.current,
416
422
  fillOpacity,
417
423
  strokeOpacity,
418
424
  strokeWidth,