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.
Files changed (49) hide show
  1. package/dist/components/Axis.svelte +25 -0
  2. package/dist/components/Axis.svelte.d.ts +10 -0
  3. package/dist/components/Circle.svelte +82 -59
  4. package/dist/components/Ellipse.svelte +83 -64
  5. package/dist/components/GeoRaster.svelte +311 -0
  6. package/dist/components/GeoRaster.svelte.d.ts +61 -0
  7. package/dist/components/Grid.svelte +15 -0
  8. package/dist/components/Grid.svelte.d.ts +5 -0
  9. package/dist/components/Image.svelte +2 -2
  10. package/dist/components/Line.svelte +82 -62
  11. package/dist/components/Points.svelte +2 -2
  12. package/dist/components/Polygon.svelte +92 -56
  13. package/dist/components/Rect.svelte +113 -64
  14. package/dist/components/Rule.svelte +2 -0
  15. package/dist/components/Sankey.svelte +0 -2
  16. package/dist/components/Text.svelte +83 -52
  17. package/dist/components/charts/PieChart.svelte +2 -2
  18. package/dist/components/index.d.ts +2 -0
  19. package/dist/components/index.js +2 -0
  20. package/dist/components/layers/Canvas.svelte +65 -48
  21. package/dist/components/layers/Canvas.svelte.d.ts +10 -0
  22. package/dist/contexts/canvas.d.ts +3 -0
  23. package/dist/server/ContextCapture.svelte +30 -0
  24. package/dist/server/ContextCapture.svelte.d.ts +8 -0
  25. package/dist/server/ServerChart.svelte +26 -0
  26. package/dist/server/ServerChart.svelte.d.ts +11 -0
  27. package/dist/server/TestBarChart.svelte +35 -0
  28. package/dist/server/TestBarChart.svelte.d.ts +14 -0
  29. package/dist/server/TestLineChart.svelte +35 -0
  30. package/dist/server/TestLineChart.svelte.d.ts +14 -0
  31. package/dist/server/captureStore.d.ts +8 -0
  32. package/dist/server/captureStore.js +18 -0
  33. package/dist/server/index.d.ts +137 -0
  34. package/dist/server/index.js +141 -0
  35. package/dist/server/renderChart.ssr.test.d.ts +1 -0
  36. package/dist/server/renderChart.ssr.test.js +205 -0
  37. package/dist/server/renderTree.d.ts +8 -0
  38. package/dist/server/renderTree.js +29 -0
  39. 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
  40. 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
  41. package/dist/states/chart.svelte.d.ts +5 -1
  42. package/dist/states/chart.svelte.js +18 -3
  43. package/dist/states/chart.svelte.test.js +110 -0
  44. package/dist/states/geo.svelte.d.ts +5 -1
  45. package/dist/states/geo.svelte.js +80 -68
  46. package/dist/utils/canvas.js +29 -10
  47. package/dist/utils/canvas.svelte.test.js +2 -2
  48. package/dist/utils/motion.svelte.js +14 -0
  49. package/package.json +7 -1
@@ -0,0 +1,14 @@
1
+ import type { CaptureTarget } from './captureStore.js';
2
+ type $$ComponentProps = {
3
+ data: {
4
+ date: number;
5
+ value: number;
6
+ }[];
7
+ width: number;
8
+ height: number;
9
+ capture?: CaptureTarget;
10
+ onCapture?: (data: CaptureTarget) => void;
11
+ };
12
+ declare const TestLineChart: import("svelte").Component<$$ComponentProps, {}, "">;
13
+ type TestLineChart = ReturnType<typeof TestLineChart>;
14
+ export default TestLineChart;
@@ -0,0 +1,8 @@
1
+ import type { ChartState, ComponentNode } from '../states/chart.svelte.js';
2
+ export type CaptureTarget = {
3
+ chartState?: ChartState;
4
+ rootNode?: ComponentNode;
5
+ };
6
+ export type SSRCapture = CaptureTarget | null;
7
+ export declare function setSSRCapture(target: SSRCapture): void;
8
+ export declare function getSSRCapture(): SSRCapture;
@@ -0,0 +1,18 @@
1
+ const SSR_CAPTURE_KEY = Symbol.for('layerchart.ssr-capture');
2
+ let _capture = null;
3
+ function getGlobalCaptureStore() {
4
+ return globalThis;
5
+ }
6
+ export function setSSRCapture(target) {
7
+ _capture = target;
8
+ const globalStore = getGlobalCaptureStore();
9
+ if (target == null) {
10
+ delete globalStore[SSR_CAPTURE_KEY];
11
+ }
12
+ else {
13
+ globalStore[SSR_CAPTURE_KEY] = target;
14
+ }
15
+ }
16
+ export function getSSRCapture() {
17
+ return getGlobalCaptureStore()[SSR_CAPTURE_KEY] ?? _capture;
18
+ }
@@ -0,0 +1,137 @@
1
+ import type { Component } from 'svelte';
2
+ import type { ChartState } from '../states/chart.svelte.js';
3
+ import type { ComponentNode } from '../states/chart.svelte.js';
4
+ import type { CaptureTarget } from './captureStore.js';
5
+ export { renderTree } from './renderTree.js';
6
+ export { default as ServerChart } from './ServerChart.svelte';
7
+ export { getSSRCapture, setSSRCapture, type CaptureTarget, type SSRCapture, } from './captureStore.js';
8
+ export type CapturedChart = {
9
+ chartState: ChartState;
10
+ rootNode: ComponentNode;
11
+ };
12
+ export type CanvasRenderContext = Omit<CanvasRenderingContext2D, 'drawFocusIfNeeded' | 'canvas'> & {
13
+ canvas?: unknown;
14
+ };
15
+ export type CanvasFactory = (width: number, height: number) => {
16
+ getContext(type: '2d'): unknown;
17
+ toBuffer(mimeType: string, ...args: any[]): Buffer | Uint8Array;
18
+ };
19
+ export type RenderOptions = {
20
+ /** Pixel ratio for high-DPI output. @default 1 */
21
+ devicePixelRatio?: number;
22
+ /** Output format. @default 'png' */
23
+ format?: 'png' | 'jpeg';
24
+ /** JPEG quality (0-1). Only used when format is 'jpeg'. @default 0.92 */
25
+ quality?: number;
26
+ /**
27
+ * Background color to fill before rendering the chart.
28
+ * When omitted, PNG output is transparent.
29
+ * Set to `'white'` (or any CSS color) for an opaque background —
30
+ * recommended for JPEG which does not support transparency.
31
+ */
32
+ background?: string;
33
+ /**
34
+ * Canvas factory function.
35
+ *
36
+ * Example with \@napi-rs/canvas:
37
+ * ```ts
38
+ * import { createCanvas } from '\@napi-rs/canvas';
39
+ * createCanvas: (w, h) => createCanvas(w, h)
40
+ * ```
41
+ */
42
+ createCanvas: CanvasFactory;
43
+ };
44
+ export type RenderChartOptions = RenderOptions & {
45
+ /** Width of the output image in pixels. */
46
+ width: number;
47
+ /** Height of the output image in pixels. */
48
+ height: number;
49
+ /** Additional props to pass to the chart component. */
50
+ props?: Record<string, any>;
51
+ };
52
+ /**
53
+ * Create a capture callback for use with `render()` from `svelte/server`.
54
+ * Pass the returned `onCapture` as the `onCapture` prop to your chart component.
55
+ * After `render()` completes, call `getCapture()` to retrieve the chart state and
56
+ * component tree.
57
+ *
58
+ * @example
59
+ * ```ts
60
+ * import { render } from 'svelte/server';
61
+ * import { createCaptureCallback, renderCapturedChart } from '../index.js/server';
62
+ * import MyChart from './MyChart.svelte';
63
+ *
64
+ * const { onCapture, getCapture } = createCaptureCallback();
65
+ * const rendered = render(MyChart, { props: { data, width: 800, height: 400, onCapture } });
66
+ * rendered.body; // Force the SSR render to fully flush before reading capture state
67
+ * const capture = getCapture();
68
+ * ```
69
+ */
70
+ export declare function createCaptureCallback(): {
71
+ onCapture: (data: CaptureTarget) => void;
72
+ getCapture: () => CaptureTarget | null;
73
+ };
74
+ /**
75
+ * Render a chart component to an image buffer in a single call.
76
+ *
77
+ * This is a convenience function that handles SSR rendering, capture, and
78
+ * canvas rendering in one step. The component should use `<ServerChart>`
79
+ * internally and accept `width`, `height`, and `capture` props.
80
+ *
81
+ * @example
82
+ * ```ts
83
+ * import { createCanvas, Path2D } from '\@napi-rs/canvas';
84
+ * import { renderChart } from '../index.js/server';
85
+ * import MyChart from './MyChart.svelte';
86
+ *
87
+ * // Register Path2D globally for canvas rendering
88
+ * if (typeof globalThis.Path2D === 'undefined') (globalThis as any).Path2D = Path2D;
89
+ *
90
+ * const buffer = renderChart(MyChart, {
91
+ * width: 800,
92
+ * height: 400,
93
+ * props: { data: myData },
94
+ * createCanvas: (w, h) => createCanvas(w, h),
95
+ * });
96
+ *
97
+ * // Use as a Response in a SvelteKit endpoint
98
+ * return new Response(buffer, {
99
+ * headers: { 'Content-Type': 'image/png' }
100
+ * });
101
+ * ```
102
+ */
103
+ export declare function renderChart(component: Component<any>, options: RenderChartOptions): Buffer | Uint8Array;
104
+ /**
105
+ * Render a captured chart component tree to an image buffer.
106
+ * Call this after `render()` from `svelte/server` has been used to build
107
+ * the component tree with a capture callback.
108
+ *
109
+ * For most use cases, prefer {@link renderChart} which handles the full pipeline.
110
+ *
111
+ * @example
112
+ * ```ts
113
+ * import { render } from 'svelte/server';
114
+ * import { createCanvas, Path2D } from '\@napi-rs/canvas';
115
+ * import { createCaptureCallback, renderCapturedChart } from '../index.js/server';
116
+ * import MyChart from './MyChart.svelte';
117
+ *
118
+ * // Register canvas globals
119
+ * if (typeof globalThis.Path2D === 'undefined') (globalThis as any).Path2D = Path2D;
120
+ *
121
+ * // Build component tree via SSR render
122
+ * const { onCapture, getCapture } = createCaptureCallback();
123
+ * const rendered = render(MyChart, { props: { data, width: 800, height: 400, onCapture } });
124
+ * rendered.body; // Force the SSR render to fully flush before reading capture state
125
+ *
126
+ * // Render to image
127
+ * const buffer = renderCapturedChart(getCapture()!, {
128
+ * width: 800,
129
+ * height: 400,
130
+ * createCanvas: (w, h) => createCanvas(w, h),
131
+ * });
132
+ * ```
133
+ */
134
+ export declare function renderCapturedChart(capture: CapturedChart, options: RenderOptions & {
135
+ width: number;
136
+ height: number;
137
+ }): Buffer | Uint8Array;
@@ -0,0 +1,141 @@
1
+ import { render } from 'svelte/server';
2
+ import { renderTree } from './renderTree.js';
3
+ export { renderTree } from './renderTree.js';
4
+ export { default as ServerChart } from './ServerChart.svelte';
5
+ export { getSSRCapture, setSSRCapture, } from './captureStore.js';
6
+ /**
7
+ * Create a capture callback for use with `render()` from `svelte/server`.
8
+ * Pass the returned `onCapture` as the `onCapture` prop to your chart component.
9
+ * After `render()` completes, call `getCapture()` to retrieve the chart state and
10
+ * component tree.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * import { render } from 'svelte/server';
15
+ * import { createCaptureCallback, renderCapturedChart } from '../index.js/server';
16
+ * import MyChart from './MyChart.svelte';
17
+ *
18
+ * const { onCapture, getCapture } = createCaptureCallback();
19
+ * const rendered = render(MyChart, { props: { data, width: 800, height: 400, onCapture } });
20
+ * rendered.body; // Force the SSR render to fully flush before reading capture state
21
+ * const capture = getCapture();
22
+ * ```
23
+ */
24
+ export function createCaptureCallback() {
25
+ let captured = null;
26
+ return {
27
+ onCapture: (data) => {
28
+ captured = data;
29
+ },
30
+ getCapture: () => captured,
31
+ };
32
+ }
33
+ /**
34
+ * Render a chart component to an image buffer in a single call.
35
+ *
36
+ * This is a convenience function that handles SSR rendering, capture, and
37
+ * canvas rendering in one step. The component should use `<ServerChart>`
38
+ * internally and accept `width`, `height`, and `capture` props.
39
+ *
40
+ * @example
41
+ * ```ts
42
+ * import { createCanvas, Path2D } from '\@napi-rs/canvas';
43
+ * import { renderChart } from '../index.js/server';
44
+ * import MyChart from './MyChart.svelte';
45
+ *
46
+ * // Register Path2D globally for canvas rendering
47
+ * if (typeof globalThis.Path2D === 'undefined') (globalThis as any).Path2D = Path2D;
48
+ *
49
+ * const buffer = renderChart(MyChart, {
50
+ * width: 800,
51
+ * height: 400,
52
+ * props: { data: myData },
53
+ * createCanvas: (w, h) => createCanvas(w, h),
54
+ * });
55
+ *
56
+ * // Use as a Response in a SvelteKit endpoint
57
+ * return new Response(buffer, {
58
+ * headers: { 'Content-Type': 'image/png' }
59
+ * });
60
+ * ```
61
+ */
62
+ export function renderChart(component, options) {
63
+ const { width, height, props = {}, ...renderOptions } = options;
64
+ const captureTarget = {};
65
+ // SSR render to build the component tree and capture chart state
66
+ const rendered = render(component, {
67
+ props: { ...props, width, height, capture: captureTarget }
68
+ });
69
+ // Force the SSR render to fully flush
70
+ void rendered.body;
71
+ if (!captureTarget.chartState || !captureTarget.rootNode) {
72
+ throw new Error('Failed to capture chart state. Ensure the component uses <ServerChart> with a `capture` prop.');
73
+ }
74
+ return renderCapturedChart(captureTarget, {
75
+ width,
76
+ height,
77
+ ...renderOptions,
78
+ });
79
+ }
80
+ /**
81
+ * Render a captured chart component tree to an image buffer.
82
+ * Call this after `render()` from `svelte/server` has been used to build
83
+ * the component tree with a capture callback.
84
+ *
85
+ * For most use cases, prefer {@link renderChart} which handles the full pipeline.
86
+ *
87
+ * @example
88
+ * ```ts
89
+ * import { render } from 'svelte/server';
90
+ * import { createCanvas, Path2D } from '\@napi-rs/canvas';
91
+ * import { createCaptureCallback, renderCapturedChart } from '../index.js/server';
92
+ * import MyChart from './MyChart.svelte';
93
+ *
94
+ * // Register canvas globals
95
+ * if (typeof globalThis.Path2D === 'undefined') (globalThis as any).Path2D = Path2D;
96
+ *
97
+ * // Build component tree via SSR render
98
+ * const { onCapture, getCapture } = createCaptureCallback();
99
+ * const rendered = render(MyChart, { props: { data, width: 800, height: 400, onCapture } });
100
+ * rendered.body; // Force the SSR render to fully flush before reading capture state
101
+ *
102
+ * // Render to image
103
+ * const buffer = renderCapturedChart(getCapture()!, {
104
+ * width: 800,
105
+ * height: 400,
106
+ * createCanvas: (w, h) => createCanvas(w, h),
107
+ * });
108
+ * ```
109
+ */
110
+ export function renderCapturedChart(capture, options) {
111
+ const { width, height, devicePixelRatio = 1, format = 'png', quality = 0.92, background, createCanvas, } = options;
112
+ // Create canvas
113
+ const canvasWidth = Math.round(width * devicePixelRatio);
114
+ const canvasHeight = Math.round(height * devicePixelRatio);
115
+ const canvas = createCanvas(canvasWidth, canvasHeight);
116
+ const ctx = canvas.getContext('2d');
117
+ // Fill background (canvas is transparent by default)
118
+ if (background) {
119
+ ctx.fillStyle = background;
120
+ ctx.fillRect(0, 0, canvasWidth, canvasHeight);
121
+ }
122
+ // Apply DPI scaling
123
+ if (devicePixelRatio !== 1) {
124
+ ctx.scale(devicePixelRatio, devicePixelRatio);
125
+ }
126
+ // Apply padding translation (mirrors what Canvas.svelte's update() does)
127
+ if (capture.chartState) {
128
+ const padding = capture.chartState.padding;
129
+ if (padding) {
130
+ ctx.translate(padding.left ?? 0, padding.top ?? 0);
131
+ }
132
+ }
133
+ // Render the component tree onto the canvas
134
+ renderTree(ctx, capture.rootNode);
135
+ // Export to buffer
136
+ const mimeType = format === 'jpeg' ? 'image/jpeg' : 'image/png';
137
+ if (format === 'jpeg') {
138
+ return canvas.toBuffer(mimeType, quality);
139
+ }
140
+ return canvas.toBuffer(mimeType);
141
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,205 @@
1
+ import { describe, it, expect, beforeAll } from 'vitest';
2
+ import { render } from 'svelte/server';
3
+ import { createCanvas, Path2D } from '@napi-rs/canvas';
4
+ import { renderChart, renderCapturedChart, createCaptureCallback, } from './index.js';
5
+ import TestLineChart from './TestLineChart.svelte';
6
+ import TestBarChart from './TestBarChart.svelte';
7
+ // Register Path2D globally for canvas rendering
8
+ beforeAll(() => {
9
+ if (typeof globalThis.Path2D === 'undefined') {
10
+ globalThis.Path2D = Path2D;
11
+ }
12
+ });
13
+ const createNodeCanvas = (w, h) => createCanvas(w, h);
14
+ const lineData = Array.from({ length: 20 }, (_, i) => ({
15
+ date: i,
16
+ value: 50 + 30 * Math.sin(i / 5),
17
+ }));
18
+ const barData = [
19
+ { category: 'A', value: 28 },
20
+ { category: 'B', value: 55 },
21
+ { category: 'C', value: 43 },
22
+ { category: 'D', value: 91 },
23
+ ];
24
+ describe('renderChart', () => {
25
+ it('renders a line chart to PNG buffer', () => {
26
+ const buffer = renderChart(TestLineChart, {
27
+ width: 400,
28
+ height: 200,
29
+ props: { data: lineData },
30
+ createCanvas: createNodeCanvas,
31
+ });
32
+ expect(buffer).toBeInstanceOf(Buffer);
33
+ expect(buffer.length).toBeGreaterThan(0);
34
+ // PNG magic bytes
35
+ expect(buffer[0]).toBe(0x89);
36
+ expect(buffer[1]).toBe(0x50); // P
37
+ expect(buffer[2]).toBe(0x4e); // N
38
+ expect(buffer[3]).toBe(0x47); // G
39
+ });
40
+ it('renders a bar chart to PNG buffer', () => {
41
+ const buffer = renderChart(TestBarChart, {
42
+ width: 400,
43
+ height: 200,
44
+ props: { data: barData },
45
+ createCanvas: createNodeCanvas,
46
+ });
47
+ expect(buffer).toBeInstanceOf(Buffer);
48
+ expect(buffer.length).toBeGreaterThan(0);
49
+ // PNG magic bytes
50
+ expect(buffer[0]).toBe(0x89);
51
+ });
52
+ it('renders to JPEG format', () => {
53
+ const buffer = renderChart(TestLineChart, {
54
+ width: 400,
55
+ height: 200,
56
+ format: 'jpeg',
57
+ props: { data: lineData },
58
+ createCanvas: createNodeCanvas,
59
+ });
60
+ expect(buffer).toBeInstanceOf(Buffer);
61
+ // JPEG magic bytes (SOI marker)
62
+ expect(buffer[0]).toBe(0xff);
63
+ expect(buffer[1]).toBe(0xd8);
64
+ });
65
+ it('respects custom dimensions', () => {
66
+ const buffer1 = renderChart(TestLineChart, {
67
+ width: 200,
68
+ height: 100,
69
+ props: { data: lineData },
70
+ createCanvas: createNodeCanvas,
71
+ });
72
+ const buffer2 = renderChart(TestLineChart, {
73
+ width: 800,
74
+ height: 600,
75
+ props: { data: lineData },
76
+ createCanvas: createNodeCanvas,
77
+ });
78
+ // Larger image should produce a larger buffer
79
+ expect(buffer2.length).toBeGreaterThan(buffer1.length);
80
+ });
81
+ it('supports devicePixelRatio', () => {
82
+ const buffer1x = renderChart(TestLineChart, {
83
+ width: 400,
84
+ height: 200,
85
+ devicePixelRatio: 1,
86
+ props: { data: lineData },
87
+ createCanvas: createNodeCanvas,
88
+ });
89
+ const buffer2x = renderChart(TestLineChart, {
90
+ width: 400,
91
+ height: 200,
92
+ devicePixelRatio: 2,
93
+ props: { data: lineData },
94
+ createCanvas: createNodeCanvas,
95
+ });
96
+ // 2x DPI should produce a larger buffer (more pixels)
97
+ expect(buffer2x.length).toBeGreaterThan(buffer1x.length);
98
+ });
99
+ it('supports background color', () => {
100
+ const transparentBuffer = renderChart(TestLineChart, {
101
+ width: 400,
102
+ height: 200,
103
+ props: { data: lineData },
104
+ createCanvas: createNodeCanvas,
105
+ });
106
+ const whiteBuffer = renderChart(TestLineChart, {
107
+ width: 400,
108
+ height: 200,
109
+ background: 'white',
110
+ props: { data: lineData },
111
+ createCanvas: createNodeCanvas,
112
+ });
113
+ // Both should be valid PNGs but different content
114
+ expect(transparentBuffer[0]).toBe(0x89);
115
+ expect(whiteBuffer[0]).toBe(0x89);
116
+ expect(Buffer.compare(transparentBuffer, whiteBuffer)).not.toBe(0);
117
+ });
118
+ it('throws on missing ServerChart', () => {
119
+ // A bare component that doesn't use ServerChart should fail
120
+ expect(() => renderChart(
121
+ // Use a dummy component-like object
122
+ (() => { }), {
123
+ width: 400,
124
+ height: 200,
125
+ createCanvas: createNodeCanvas,
126
+ })).toThrow('Failed to capture chart state');
127
+ });
128
+ });
129
+ describe('renderCapturedChart', () => {
130
+ it('renders a captured chart tree to a buffer', () => {
131
+ const captureTarget = {};
132
+ const rendered = render(TestLineChart, {
133
+ props: { data: lineData, width: 400, height: 200, capture: captureTarget },
134
+ });
135
+ void rendered.body;
136
+ expect(captureTarget.chartState).toBeDefined();
137
+ expect(captureTarget.rootNode).toBeDefined();
138
+ const buffer = renderCapturedChart(captureTarget, {
139
+ width: 400,
140
+ height: 200,
141
+ createCanvas: createNodeCanvas,
142
+ });
143
+ expect(buffer).toBeInstanceOf(Buffer);
144
+ expect(buffer[0]).toBe(0x89); // PNG
145
+ });
146
+ });
147
+ describe('createCaptureCallback', () => {
148
+ it('captures chart state via callback', () => {
149
+ const { onCapture, getCapture } = createCaptureCallback();
150
+ const rendered = render(TestLineChart, {
151
+ props: { data: lineData, width: 400, height: 200, onCapture },
152
+ });
153
+ void rendered.body;
154
+ const capture = getCapture();
155
+ expect(capture).not.toBeNull();
156
+ expect(capture?.chartState).toBeDefined();
157
+ expect(capture?.rootNode).toBeDefined();
158
+ });
159
+ it('returns null before render', () => {
160
+ const { getCapture } = createCaptureCallback();
161
+ expect(getCapture()).toBeNull();
162
+ });
163
+ });
164
+ describe('ServerChart capture prop', () => {
165
+ it('populates capture target via prop', () => {
166
+ const captureTarget = {};
167
+ const rendered = render(TestLineChart, {
168
+ props: { data: lineData, width: 400, height: 200, capture: captureTarget },
169
+ });
170
+ void rendered.body;
171
+ expect(captureTarget.chartState).toBeDefined();
172
+ expect(captureTarget.rootNode).toBeDefined();
173
+ expect(captureTarget.rootNode.children.length).toBeGreaterThan(0);
174
+ });
175
+ it('captures chart state with correct padding', () => {
176
+ const captureTarget = {};
177
+ const rendered = render(TestLineChart, {
178
+ props: { data: lineData, width: 800, height: 400, capture: captureTarget },
179
+ });
180
+ void rendered.body;
181
+ const state = captureTarget.chartState;
182
+ expect(state.padding).toEqual({ top: 20, right: 20, bottom: 20, left: 20 });
183
+ });
184
+ it('captures component tree with children', () => {
185
+ const captureTarget = {};
186
+ const rendered = render(TestLineChart, {
187
+ props: { data: lineData, width: 400, height: 200, capture: captureTarget },
188
+ });
189
+ void rendered.body;
190
+ // Root node (Canvas) should have children
191
+ const root = captureTarget.rootNode;
192
+ expect(root.kind).toBe('group');
193
+ expect(root.children.length).toBeGreaterThan(0);
194
+ // Count all marks in the tree (may be nested in composite-marks)
195
+ function countMarks(node) {
196
+ let count = node.kind === 'mark' ? 1 : 0;
197
+ for (const child of node.children) {
198
+ count += countMarks(child);
199
+ }
200
+ return count;
201
+ }
202
+ // Should have at least 2 marks (Area and Spline)
203
+ expect(countMarks(root)).toBeGreaterThanOrEqual(2);
204
+ });
205
+ });
@@ -0,0 +1,8 @@
1
+ import type { ComponentNode } from '../states/chart.svelte.js';
2
+ /**
3
+ * Recursively render the component tree onto a canvas context.
4
+ * Group nodes: save → render → recurse children → restore
5
+ * Leaf nodes: save → render → restore
6
+ * Non-rendering nodes: just recurse children
7
+ */
8
+ export declare function renderTree(ctx: CanvasRenderingContext2D, node: ComponentNode): void;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Recursively render the component tree onto a canvas context.
3
+ * Group nodes: save → render → recurse children → restore
4
+ * Leaf nodes: save → render → restore
5
+ * Non-rendering nodes: just recurse children
6
+ */
7
+ export function renderTree(ctx, node) {
8
+ if (node.kind === 'group' && node.canvasRender) {
9
+ // Group: save state, apply transform, render children, restore
10
+ ctx.save();
11
+ node.canvasRender.render(ctx);
12
+ for (const child of node.children) {
13
+ renderTree(ctx, child);
14
+ }
15
+ ctx.restore();
16
+ }
17
+ else if (node.canvasRender) {
18
+ // Leaf mark: save, render, restore
19
+ ctx.save();
20
+ node.canvasRender.render(ctx);
21
+ ctx.restore();
22
+ }
23
+ else {
24
+ // Non-rendering node (e.g. root, composite-mark): just recurse children
25
+ for (const child of node.children) {
26
+ renderTree(ctx, child);
27
+ }
28
+ }
29
+ }
@@ -64,7 +64,11 @@ export declare class ChartState<TData = any, XScale extends AnyScale = AnyScale,
64
64
  private _markInfosVersion;
65
65
  private _nextMarkId;
66
66
  /** Reactive accessor — reads _markInfosVersion to create a reactive dependency,
67
- * returns the plain array so items are never wrapped in Svelte proxies. */
67
+ * returns the plain array so items are never wrapped in Svelte proxies.
68
+ *
69
+ * When a geo projection is active, strips x/y/data from mark info — those
70
+ * values are geographic coordinates handled by the projection, not xScale/yScale.
71
+ * seriesKey/color/label are preserved so marks can still contribute to legends. */
68
72
  private get _markInfos();
69
73
  /**
70
74
  * Register a mark with the chart. The MarkInfo snapshot is stored directly
@@ -48,10 +48,24 @@ export class ChartState {
48
48
  _markInfosVersion = $state(0);
49
49
  _nextMarkId = 0;
50
50
  /** Reactive accessor — reads _markInfosVersion to create a reactive dependency,
51
- * returns the plain array so items are never wrapped in Svelte proxies. */
51
+ * returns the plain array so items are never wrapped in Svelte proxies.
52
+ *
53
+ * When a geo projection is active, strips x/y/data from mark info — those
54
+ * values are geographic coordinates handled by the projection, not xScale/yScale.
55
+ * seriesKey/color/label are preserved so marks can still contribute to legends. */
52
56
  get _markInfos() {
53
57
  // eslint-disable-next-line @typescript-eslint/no-unused-expressions
54
58
  this._markInfosVersion;
59
+ if (this.geoState.props.projection) {
60
+ return this._markInfosRaw.map(({ _id, info }) => ({
61
+ _id,
62
+ info: {
63
+ seriesKey: info.seriesKey,
64
+ color: info.color,
65
+ label: info.label,
66
+ },
67
+ }));
68
+ }
55
69
  return this._markInfosRaw;
56
70
  }
57
71
  /**
@@ -147,8 +161,9 @@ export class ChartState {
147
161
  meta = $derived(this.props.meta ?? {});
148
162
  constructor(propsGetter) {
149
163
  this._propsGetter = propsGetter;
150
- // Create GeoState instance
151
- this.geoState = new GeoState(() => this.props.geo ?? {});
164
+ // Create GeoState instance — pass a dimensions getter so projection
165
+ // is available during SSR (where $effect doesn't run)
166
+ this.geoState = new GeoState(() => this.props.geo ?? {}, () => ({ width: this.width, height: this.height }));
152
167
  // Create SeriesState internally from series/seriesLayout props.
153
168
  // When no explicit series are provided, derive implicit series from mark registrations.
154
169
  this.seriesState = new SeriesState(() => {