layerchart 2.0.0-next.50 → 2.0.0-next.52

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 (70) hide show
  1. package/dist/components/Arc.svelte +12 -4
  2. package/dist/components/Arc.svelte.d.ts +4 -0
  3. package/dist/components/ArcLabel.svelte +259 -0
  4. package/dist/components/ArcLabel.svelte.d.ts +73 -0
  5. package/dist/components/ArcLabel.svelte.test.d.ts +1 -0
  6. package/dist/components/ArcLabel.svelte.test.js +235 -0
  7. package/dist/components/Axis.svelte +25 -0
  8. package/dist/components/Axis.svelte.d.ts +10 -0
  9. package/dist/components/Circle.svelte +82 -59
  10. package/dist/components/CircleLegend.svelte +389 -0
  11. package/dist/components/CircleLegend.svelte.d.ts +114 -0
  12. package/dist/components/Ellipse.svelte +83 -64
  13. package/dist/components/GeoLegend.svelte +404 -0
  14. package/dist/components/GeoLegend.svelte.d.ts +106 -0
  15. package/dist/components/GeoRaster.svelte +311 -0
  16. package/dist/components/GeoRaster.svelte.d.ts +61 -0
  17. package/dist/components/Grid.svelte +15 -0
  18. package/dist/components/Grid.svelte.d.ts +5 -0
  19. package/dist/components/Image.svelte +2 -2
  20. package/dist/components/Labels.svelte +46 -11
  21. package/dist/components/Labels.svelte.d.ts +7 -3
  22. package/dist/components/Legend.svelte +58 -3
  23. package/dist/components/Legend.svelte.d.ts +7 -0
  24. package/dist/components/Line.svelte +82 -62
  25. package/dist/components/Points.svelte +2 -2
  26. package/dist/components/Polygon.svelte +92 -56
  27. package/dist/components/Rect.svelte +113 -64
  28. package/dist/components/Rule.svelte +2 -0
  29. package/dist/components/Sankey.svelte +0 -2
  30. package/dist/components/Text.svelte +83 -52
  31. package/dist/components/__screenshots__/ArcLabel.svelte.test.ts/ArcLabel-defaults-placement-to-centroid--x-y-at-the-centroid--middle-anchors--1.png +0 -0
  32. package/dist/components/__screenshots__/ArcLabel.svelte.test.ts/ArcLabel-defaults-placement-to-centroid--x-y-at-the-centroid--middle-anchors--2.png +0 -0
  33. package/dist/components/charts/ArcChart.svelte +39 -2
  34. package/dist/components/charts/ArcChart.svelte.d.ts +12 -1
  35. package/dist/components/charts/PieChart.svelte +40 -2
  36. package/dist/components/charts/PieChart.svelte.d.ts +10 -0
  37. package/dist/components/index.d.ts +8 -0
  38. package/dist/components/index.js +8 -0
  39. package/dist/components/layers/Canvas.svelte +65 -48
  40. package/dist/components/layers/Canvas.svelte.d.ts +10 -0
  41. package/dist/contexts/canvas.d.ts +3 -0
  42. package/dist/server/ContextCapture.svelte +30 -0
  43. package/dist/server/ContextCapture.svelte.d.ts +8 -0
  44. package/dist/server/ServerChart.svelte +26 -0
  45. package/dist/server/ServerChart.svelte.d.ts +11 -0
  46. package/dist/server/TestBarChart.svelte +35 -0
  47. package/dist/server/TestBarChart.svelte.d.ts +14 -0
  48. package/dist/server/TestLineChart.svelte +35 -0
  49. package/dist/server/TestLineChart.svelte.d.ts +14 -0
  50. package/dist/server/captureStore.d.ts +8 -0
  51. package/dist/server/captureStore.js +18 -0
  52. package/dist/server/index.d.ts +137 -0
  53. package/dist/server/index.js +141 -0
  54. package/dist/server/renderChart.ssr.test.d.ts +1 -0
  55. package/dist/server/renderChart.ssr.test.js +205 -0
  56. package/dist/server/renderTree.d.ts +8 -0
  57. package/dist/server/renderTree.js +29 -0
  58. 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
  59. 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
  60. package/dist/states/chart.svelte.d.ts +5 -1
  61. package/dist/states/chart.svelte.js +18 -3
  62. package/dist/states/chart.svelte.test.js +110 -0
  63. package/dist/states/geo.svelte.d.ts +5 -1
  64. package/dist/states/geo.svelte.js +80 -68
  65. package/dist/utils/arcText.svelte.d.ts +7 -1
  66. package/dist/utils/arcText.svelte.js +8 -4
  67. package/dist/utils/canvas.js +29 -10
  68. package/dist/utils/canvas.svelte.test.js +2 -2
  69. package/dist/utils/motion.svelte.js +14 -0
  70. package/package.json +7 -1
@@ -1,6 +1,7 @@
1
1
  import { describe, it, expect } from 'vitest';
2
2
  import { flushSync } from 'svelte';
3
3
  import { scaleBand } from 'd3-scale';
4
+ import { geoAlbersUsa } from 'd3-geo';
4
5
  import { timeDay } from 'd3-time';
5
6
  import { ChartState } from './chart.svelte.js';
6
7
  import { isScaleBand, isScaleTime } from '../utils/scales.svelte.js';
@@ -527,6 +528,115 @@ describe('ChartState mark registration', () => {
527
528
  }
528
529
  });
529
530
  });
531
+ describe('ChartState geo projection skips markInfo', () => {
532
+ const geoData = [
533
+ { name: 'New York', longitude: -74.006, latitude: 40.7128 },
534
+ { name: 'Los Angeles', longitude: -118.2437, latitude: 34.0522 },
535
+ { name: 'Chicago', longitude: -87.6298, latitude: 41.8781 },
536
+ ];
537
+ it('should not create implicit series from marks when geo projection is active', () => {
538
+ const { state, cleanup } = createChartState({
539
+ data: geoData,
540
+ x: 'longitude',
541
+ y: 'latitude',
542
+ geo: { projection: geoAlbersUsa },
543
+ });
544
+ try {
545
+ // Register a mark with its own data (like a tooltip highlight Circle)
546
+ state.registerMark({ data: [geoData[0]], x: 'longitude', y: 'latitude' });
547
+ flushSync();
548
+ // Should remain default series — mark should not create implicit "latitude" series
549
+ expect(state.seriesState.isDefaultSeries).toBe(true);
550
+ }
551
+ finally {
552
+ cleanup();
553
+ }
554
+ });
555
+ it('should not add mark data to flatData when geo projection is active', () => {
556
+ const { state, cleanup } = createChartState({
557
+ data: geoData,
558
+ x: 'longitude',
559
+ y: 'latitude',
560
+ geo: { projection: geoAlbersUsa },
561
+ });
562
+ try {
563
+ state.registerMark({ data: [geoData[0]], x: 'longitude', y: 'latitude' });
564
+ flushSync();
565
+ // flatData should only contain chart data, not the mark's extra data
566
+ expect(state.flatData).toHaveLength(3);
567
+ expect(state.flatData).toBe(geoData);
568
+ }
569
+ finally {
570
+ cleanup();
571
+ }
572
+ });
573
+ it('should not derive x/y accessors from marks when geo projection is active', () => {
574
+ // Chart with geo but no explicit x/y — marks should not fill in the accessors
575
+ const { state: stateWithGeo, cleanup: cleanupGeo } = createChartState({
576
+ data: geoData,
577
+ geo: { projection: geoAlbersUsa },
578
+ });
579
+ const { state: stateWithoutGeo, cleanup: cleanupNoGeo } = createChartState({
580
+ data: geoData,
581
+ });
582
+ try {
583
+ // Both start with null x accessor (no x prop set)
584
+ expect(stateWithGeo.x).toBeNull();
585
+ expect(stateWithoutGeo.x).toBeNull();
586
+ stateWithGeo.registerMark({ x: 'longitude', y: 'latitude' });
587
+ stateWithoutGeo.registerMark({ x: 'longitude', y: 'latitude' });
588
+ flushSync();
589
+ // Without geo: mark should derive x accessor
590
+ expect(stateWithoutGeo.x).not.toBeNull();
591
+ expect(stateWithoutGeo.x(geoData[0])).toBe(geoData[0].longitude);
592
+ // With geo: mark should NOT derive x accessor
593
+ expect(stateWithGeo.x).toBeNull();
594
+ }
595
+ finally {
596
+ cleanupGeo();
597
+ cleanupNoGeo();
598
+ }
599
+ });
600
+ it('should preserve seriesKey/color/label from marks in geo mode for legends', () => {
601
+ const { state, cleanup } = createChartState({
602
+ data: geoData,
603
+ x: 'longitude',
604
+ y: 'latitude',
605
+ geo: { projection: geoAlbersUsa },
606
+ });
607
+ try {
608
+ state.registerMark({ seriesKey: 'earthquakes', color: 'red', label: 'Earthquakes' });
609
+ state.registerMark({ seriesKey: 'volcanos', color: 'orange', label: 'Volcanos' });
610
+ flushSync();
611
+ // seriesKey/color/label should still create implicit series for legends
612
+ expect(state.seriesState.isDefaultSeries).toBe(false);
613
+ expect(state.seriesState.series).toHaveLength(2);
614
+ expect(state.seriesState.series[0]).toMatchObject({ key: 'earthquakes', color: 'red', label: 'Earthquakes' });
615
+ expect(state.seriesState.series[1]).toMatchObject({ key: 'volcanos', color: 'orange', label: 'Volcanos' });
616
+ // But flatData should not include extra mark data
617
+ expect(state.flatData).toHaveLength(3);
618
+ }
619
+ finally {
620
+ cleanup();
621
+ }
622
+ });
623
+ it('should still process marks normally without geo projection', () => {
624
+ const { state, cleanup } = createChartState({
625
+ data: geoData,
626
+ x: 'name',
627
+ });
628
+ try {
629
+ state.registerMark({ y: 'latitude', color: 'blue' });
630
+ flushSync();
631
+ // Without geo, marks should create implicit series as normal
632
+ expect(state.seriesState.isDefaultSeries).toBe(false);
633
+ expect(state.seriesState.series[0].key).toBe('latitude');
634
+ }
635
+ finally {
636
+ cleanup();
637
+ }
638
+ });
639
+ });
530
640
  describe('ChartState implicit series domain update on visibility toggle', () => {
531
641
  it('should update y domain when hiding an implicit series', () => {
532
642
  const data = [
@@ -30,6 +30,7 @@ export type GeoStateProps = {
30
30
  };
31
31
  export declare class GeoState {
32
32
  private _propsGetter;
33
+ private _dimensionsGetter?;
33
34
  props: GeoStateProps;
34
35
  chartWidth: number;
35
36
  chartHeight: number;
@@ -40,6 +41,9 @@ export declare class GeoState {
40
41
  translate: boolean;
41
42
  };
42
43
  projection: GeoProjection | undefined;
43
- constructor(propsGetter: () => GeoStateProps);
44
+ constructor(propsGetter: () => GeoStateProps, dimensionsGetter?: () => {
45
+ width: number;
46
+ height: number;
47
+ });
44
48
  fitSizeRange: [number, number];
45
49
  }
@@ -1,88 +1,100 @@
1
1
  export class GeoState {
2
2
  // Props getter function - set in constructor
3
3
  _propsGetter;
4
+ _dimensionsGetter;
4
5
  // Props - accessed via getter function for fine-grained reactivity
5
6
  props = $derived(this._propsGetter());
6
- // Context references
7
+ // Context references — used by GeoProjection.svelte (client-side only)
7
8
  chartWidth = $state(100);
8
9
  chartHeight = $state(100);
9
10
  transformState = $state(null);
10
11
  transformApply = $state({ rotation: false, scale: true, translate: true });
11
- // The actual projection instance
12
- projection = $state(undefined);
13
- constructor(propsGetter) {
14
- this._propsGetter = propsGetter;
15
- // Main effect to build and configure the projection
16
- $effect.pre(() => {
17
- if (!this.props.projection)
18
- return;
19
- const _projection = this.props.projection();
20
- // Apply fitSize if fitGeojson is provided
21
- if (this.props.fitGeojson && 'fitSize' in _projection) {
22
- _projection.fitSize(this.fitSizeRange, this.props.fitGeojson);
23
- }
24
- // Apply scale
25
- if ('scale' in _projection) {
26
- if (this.props.scale) {
27
- _projection.scale(this.props.scale);
28
- }
29
- if (this.transformState?.mode === 'projection' && this.transformApply.scale) {
30
- _projection.scale(this.transformState.scale);
31
- }
12
+ // The actual projection instance — derived so it works during SSR
13
+ projection = $derived.by(() => {
14
+ if (!this.props.projection)
15
+ return undefined;
16
+ const _projection = this.props.projection();
17
+ // Apply fitSize if fitGeojson is provided
18
+ if (this.props.fitGeojson && 'fitSize' in _projection) {
19
+ _projection.fitSize(this.fitSizeRange, this.props.fitGeojson);
20
+ }
21
+ // Apply scale
22
+ if ('scale' in _projection) {
23
+ if (this.props.scale) {
24
+ _projection.scale(this.props.scale);
32
25
  }
33
- // Apply rotate
34
- if ('rotate' in _projection) {
35
- if (this.props.rotate) {
36
- _projection.rotate([
37
- this.props.rotate.yaw,
38
- this.props.rotate.pitch,
39
- this.props.rotate.roll,
40
- ]);
41
- }
42
- if (this.transformState?.mode === 'projection' && this.transformApply.rotation) {
43
- _projection.rotate([
44
- this.transformState.translate.x, // yaw
45
- this.transformState.translate.y, // pitch
46
- ]);
47
- }
26
+ if (this.transformState?.mode === 'projection' && this.transformApply.scale) {
27
+ _projection.scale(this.transformState.scale);
48
28
  }
49
- // Apply translate
50
- if ('translate' in _projection) {
51
- if (this.props.translate) {
52
- _projection.translate(this.props.translate);
53
- }
54
- if (this.transformState?.mode === 'projection' && this.transformApply.translate) {
55
- _projection.translate([
56
- this.transformState.translate.x,
57
- this.transformState.translate.y,
58
- ]);
59
- }
29
+ }
30
+ // Apply rotate
31
+ if ('rotate' in _projection) {
32
+ if (this.props.rotate) {
33
+ _projection.rotate([
34
+ this.props.rotate.yaw,
35
+ this.props.rotate.pitch,
36
+ this.props.rotate.roll,
37
+ ]);
60
38
  }
61
- // Apply center
62
- if (this.props.center && 'center' in _projection) {
63
- _projection.center(this.props.center);
39
+ if (this.transformState?.mode === 'projection' && this.transformApply.rotation) {
40
+ _projection.rotate([
41
+ this.transformState.translate.x, // yaw
42
+ this.transformState.translate.y, // pitch
43
+ ]);
64
44
  }
65
- // Apply reflectX
66
- if (this.props.reflectX) {
67
- _projection.reflectX(this.props.reflectX);
45
+ }
46
+ // Apply translate
47
+ if ('translate' in _projection) {
48
+ if (this.props.translate) {
49
+ _projection.translate(this.props.translate);
68
50
  }
69
- // Apply reflectY
70
- if (this.props.reflectY) {
71
- _projection.reflectY(this.props.reflectY);
51
+ else if (!this.props.fitGeojson) {
52
+ // Default translate to container center when not explicitly set
53
+ // and not already positioned via fitSize/fitGeojson
54
+ _projection.translate([
55
+ (this._dimensionsGetter?.().width ?? this.chartWidth) / 2,
56
+ (this._dimensionsGetter?.().height ?? this.chartHeight) / 2,
57
+ ]);
72
58
  }
73
- // Apply clipAngle
74
- if (this.props.clipAngle && 'clipAngle' in _projection) {
75
- _projection.clipAngle(this.props.clipAngle);
59
+ if (this.transformState?.mode === 'projection' && this.transformApply.translate) {
60
+ _projection.translate([
61
+ this.transformState.translate.x,
62
+ this.transformState.translate.y,
63
+ ]);
76
64
  }
77
- // Apply clipExtent
78
- if (this.props.clipExtent && 'clipExtent' in _projection) {
79
- _projection.clipExtent(this.props.clipExtent);
80
- }
81
- this.projection = _projection;
82
- });
65
+ }
66
+ // Apply center
67
+ if (this.props.center && 'center' in _projection) {
68
+ _projection.center(this.props.center);
69
+ }
70
+ // Apply reflectX
71
+ if (this.props.reflectX) {
72
+ _projection.reflectX(this.props.reflectX);
73
+ }
74
+ // Apply reflectY
75
+ if (this.props.reflectY) {
76
+ _projection.reflectY(this.props.reflectY);
77
+ }
78
+ // Apply clipAngle
79
+ if (this.props.clipAngle && 'clipAngle' in _projection) {
80
+ _projection.clipAngle(this.props.clipAngle);
81
+ }
82
+ // Apply clipExtent
83
+ if (this.props.clipExtent && 'clipExtent' in _projection) {
84
+ _projection.clipExtent(this.props.clipExtent);
85
+ }
86
+ return _projection;
87
+ });
88
+ constructor(propsGetter, dimensionsGetter) {
89
+ this._propsGetter = propsGetter;
90
+ this._dimensionsGetter = dimensionsGetter;
83
91
  }
84
- // Derived properties
92
+ // Derived properties — use dimensions getter (from ChartState) when available,
93
+ // falling back to $state values (set by GeoProjection.svelte via $effect)
85
94
  fitSizeRange = $derived(this.props.fixedAspectRatio
86
95
  ? [100, 100 / this.props.fixedAspectRatio]
87
- : [this.chartWidth, this.chartHeight]);
96
+ : [
97
+ this._dimensionsGetter?.().width ?? this.chartWidth,
98
+ this._dimensionsGetter?.().height ?? this.chartHeight,
99
+ ]);
88
100
  }
@@ -39,9 +39,15 @@ export type ArcTextOptions = {
39
39
  startOffset?: string;
40
40
  /**
41
41
  * An amount of padding to add to the outer radius of the path to add space
42
- * between the text and the arc.
42
+ * between the text and the arc. Applies to `'outer'` and `'middle'` positions.
43
43
  */
44
44
  outerPadding?: number;
45
+ /**
46
+ * An amount of padding to subtract from the inner radius of the path to add
47
+ * space between the text and the arc. Applies to `'inner'` and `'middle'`
48
+ * positions. Positive values move the text inward (toward the chart center).
49
+ */
50
+ innerPadding?: number;
45
51
  /**
46
52
  * Optional offset specifically for 'outer-radial' position from the outer arc edge.
47
53
  * If not provided, 'outerPadding' will be used.
@@ -172,14 +172,18 @@ export function createArcTextProps(props, opts = {}, position) {
172
172
  return undefined;
173
173
  });
174
174
  const sharedProps = $derived.by(() => {
175
- if (reverseText) {
175
+ // Center text along the arc path by default (50% startOffset, middle anchor).
176
+ // When the caller provides an explicit `startOffset`, honor it with a `start`
177
+ // anchor so the text begins at that position instead of centering around it.
178
+ if (opts.startOffset != null) {
176
179
  return {
177
- startOffset: opts.startOffset ?? '100%',
178
- textAnchor: 'end',
180
+ startOffset: opts.startOffset,
181
+ textAnchor: 'start',
179
182
  };
180
183
  }
181
184
  return {
182
- startOffset: opts.startOffset ?? undefined,
185
+ startOffset: '50%',
186
+ textAnchor: 'middle',
183
187
  };
184
188
  });
185
189
  const radialPositionProps = $derived.by(() => {
@@ -56,6 +56,15 @@ const supportedStyles = [
56
56
  * Appends or reuses `<svg>` element below `<canvas>` to resolve CSS variables and classes (ex. `stroke: var(--color-primary)` => `stroke: rgb(...)` )
57
57
  */
58
58
  export function _getComputedStyles(canvas, { styles, classes } = {}) {
59
+ // Server-side: no DOM available, return styles with sensible defaults
60
+ if (typeof document === 'undefined') {
61
+ const merged = { ...styles };
62
+ if (!merged.fontSize)
63
+ merged.fontSize = '10px';
64
+ if (!merged.fontFamily)
65
+ merged.fontFamily = 'sans-serif';
66
+ return merged;
67
+ }
59
68
  // console.count(`getComputedStyles: ${getComputedStylesKey(canvas, { styles, classes })}`);
60
69
  try {
61
70
  // Get or create `<svg>` below `<canvas>`
@@ -111,10 +120,17 @@ function render(ctx, render, styleOptions = {}, { applyText, } = {}) {
111
120
  const mergedStyles = { ...styleOptions.styles, ...parsedInlineStyles };
112
121
  // TODO: Consider memoizing? How about reactiving to CSS variable changes (light/dark mode toggle)
113
122
  let resolvedStyles;
114
- if (styleOptions.classes == null &&
115
- !Object.values(mergedStyles).some((v) => typeof v === 'string' && v.includes('var('))) {
116
- // Skip resolving styles if no classes are provided and no styles are using CSS variables
123
+ if (typeof document === 'undefined' ||
124
+ (styleOptions.classes == null &&
125
+ !Object.values(mergedStyles).some((v) => typeof v === 'string' && v.includes('var(')))) {
126
+ // Skip resolving styles if running on server (no DOM), or no classes are provided and no styles are using CSS variables
117
127
  resolvedStyles = mergedStyles;
128
+ // On server, provide sensible defaults for styles that would normally come from CSS
129
+ if (typeof document === 'undefined') {
130
+ if (!resolvedStyles.stroke && !resolvedStyles.fill) {
131
+ resolvedStyles = { ...resolvedStyles, stroke: 'black' };
132
+ }
133
+ }
118
134
  }
119
135
  else {
120
136
  // Remove constant non-css variable properties (ex. `strokeWidth: 0.5`, `fill: #123456`) as not needed and improves memoization cache hit
@@ -140,8 +156,11 @@ function render(ctx, render, styleOptions = {}, { applyText, } = {}) {
140
156
  }
141
157
  // font/text properties can be expensive to set (not sure why), so only apply if needed (renderText())
142
158
  if (applyText) {
143
- // Text properties
144
- ctx.font = `${resolvedStyles.fontWeight} ${resolvedStyles.fontSize} ${resolvedStyles.fontFamily}`; // build string instead of using `computedStyles.font` to fix/workaround `tabular-nums` returning `null`
159
+ // Text properties — use defaults for server-side rendering where computed styles aren't available
160
+ const fontSize = resolvedStyles.fontSize || '10px';
161
+ const fontFamily = resolvedStyles.fontFamily || 'sans-serif';
162
+ const fontWeight = resolvedStyles.fontWeight || '';
163
+ ctx.font = `${fontWeight} ${fontSize} ${fontFamily}`.trim(); // build string instead of using `computedStyles.font` to fix/workaround `tabular-nums` returning `null`
145
164
  if (resolvedStyles.textAnchor === 'middle') {
146
165
  ctx.textAlign = 'center';
147
166
  }
@@ -176,8 +195,8 @@ function render(ctx, render, styleOptions = {}, { applyText, } = {}) {
176
195
  for (const attr of paintOrder) {
177
196
  if (attr === 'fill') {
178
197
  const fill = styleOptions.styles?.fill &&
179
- (styleOptions.styles?.fill instanceof CanvasGradient ||
180
- styleOptions.styles?.fill instanceof CanvasPattern ||
198
+ ((typeof CanvasGradient !== 'undefined' && styleOptions.styles?.fill instanceof CanvasGradient) ||
199
+ (typeof CanvasPattern !== 'undefined' && styleOptions.styles?.fill instanceof CanvasPattern) ||
181
200
  !styleOptions.styles?.fill?.includes('var'))
182
201
  ? styleOptions.styles.fill
183
202
  : resolvedStyles?.fill;
@@ -194,7 +213,7 @@ function render(ctx, render, styleOptions = {}, { applyText, } = {}) {
194
213
  }
195
214
  else if (attr === 'stroke') {
196
215
  const stroke = styleOptions.styles?.stroke &&
197
- (styleOptions.styles?.stroke instanceof CanvasGradient ||
216
+ ((typeof CanvasGradient !== 'undefined' && styleOptions.styles?.stroke instanceof CanvasGradient) ||
198
217
  !styleOptions.styles?.stroke?.includes('var'))
199
218
  ? styleOptions.styles?.stroke
200
219
  : resolvedStyles?.stroke;
@@ -312,7 +331,7 @@ export function clearCanvasContext(ctx, options) {
312
331
  @see: https://web.dev/articles/canvas-hidipi
313
332
  */
314
333
  export function scaleCanvas(ctx, width, height) {
315
- const devicePixelRatio = window.devicePixelRatio || 1;
334
+ const devicePixelRatio = typeof window !== 'undefined' ? (window.devicePixelRatio || 1) : 1;
316
335
  ctx.canvas.width = width * devicePixelRatio;
317
336
  ctx.canvas.height = height * devicePixelRatio;
318
337
  ctx.canvas.style.width = `${width}px`;
@@ -322,7 +341,7 @@ export function scaleCanvas(ctx, width, height) {
322
341
  }
323
342
  /** Get pixel color (r,g,b,a) at canvas coordinates */
324
343
  export function getPixelColor(ctx, x, y) {
325
- const dpr = window.devicePixelRatio ?? 1;
344
+ const dpr = (typeof window !== 'undefined' ? window.devicePixelRatio : null) ?? 1;
326
345
  const imageData = ctx.getImageData(x * dpr, y * dpr, 1, 1);
327
346
  const [r, g, b, a] = imageData.data;
328
347
  return { r, g, b, a };
@@ -270,9 +270,9 @@ describe('renderPathData', () => {
270
270
  it('applies strokeOpacity less than 1', () => {
271
271
  const globalAlphaValues = [];
272
272
  const originalStroke = ctx.stroke.bind(ctx);
273
- vi.spyOn(ctx, 'stroke').mockImplementation((...args) => {
273
+ vi.spyOn(ctx, 'stroke').mockImplementation(function () {
274
274
  globalAlphaValues.push(ctx.globalAlpha);
275
- originalStroke(...args);
275
+ originalStroke();
276
276
  });
277
277
  renderPathData(ctx, 'M0,0 L100,0', {
278
278
  styles: {
@@ -62,6 +62,20 @@ class MotionNone {
62
62
  function setupTracking(motion, getValue, options) {
63
63
  if (options.controlled)
64
64
  return;
65
+ // On the server (SSR), $effect won't run, so eagerly set the target value
66
+ // to ensure render closures capture the actual computed state (not the initial/baseline value).
67
+ if (typeof window === 'undefined') {
68
+ try {
69
+ const value = getValue();
70
+ if (value != null) {
71
+ motion.set(value, { instant: true });
72
+ }
73
+ }
74
+ catch {
75
+ // getValue() may fail if reactive dependencies aren't ready yet; ignore
76
+ }
77
+ return;
78
+ }
65
79
  $effect(() => {
66
80
  const value = getValue();
67
81
  if (value == null)
package/package.json CHANGED
@@ -5,9 +5,10 @@
5
5
  "license": "MIT",
6
6
  "repository": "techniq/layerchart",
7
7
  "homepage": "https://layerchart.com",
8
- "version": "2.0.0-next.50",
8
+ "version": "2.0.0-next.52",
9
9
  "devDependencies": {
10
10
  "@changesets/cli": "^2.30.0",
11
+ "@napi-rs/canvas": "^0.1.97",
11
12
  "@sveltejs/adapter-auto": "^7.0.1",
12
13
  "@sveltejs/kit": "^2.55.0",
13
14
  "@sveltejs/package": "^2.5.7",
@@ -89,6 +90,11 @@
89
90
  "svelte": "./dist/index.js",
90
91
  "default": "./dist/index.js"
91
92
  },
93
+ "./server": {
94
+ "types": "./dist/server/index.d.ts",
95
+ "svelte": "./dist/server/index.js",
96
+ "default": "./dist/server/index.js"
97
+ },
92
98
  "./utils/*": {
93
99
  "types": "./dist/utils/*.d.ts",
94
100
  "svelte": "./dist/utils/*.js",