layerchart 2.0.0-next.54 → 2.0.0-next.55

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 (37) hide show
  1. package/dist/components/Axis.svelte +10 -2
  2. package/dist/components/Axis.svelte.d.ts +8 -2
  3. package/dist/components/Bar.svelte +10 -38
  4. package/dist/components/Circle.svelte +23 -3
  5. package/dist/components/Circle.svelte.d.ts +6 -0
  6. package/dist/components/CircleClipPath.svelte +13 -31
  7. package/dist/components/CircleClipPath.svelte.d.ts +7 -1
  8. package/dist/components/ClipPath.svelte +64 -21
  9. package/dist/components/ClipPath.svelte.d.ts +21 -12
  10. package/dist/components/Connector.svelte +18 -0
  11. package/dist/components/Connector.svelte.d.ts +5 -0
  12. package/dist/components/GeoClipPath.svelte +14 -17
  13. package/dist/components/GeoClipPath.svelte.d.ts +6 -0
  14. package/dist/components/Grid.svelte +15 -4
  15. package/dist/components/Grid.svelte.d.ts +14 -4
  16. package/dist/components/Highlight.svelte +1 -0
  17. package/dist/components/Line.svelte +30 -3
  18. package/dist/components/Line.svelte.d.ts +7 -0
  19. package/dist/components/Link.svelte +9 -0
  20. package/dist/components/Rect.svelte +98 -7
  21. package/dist/components/Rect.svelte.d.ts +13 -1
  22. package/dist/components/RectClipPath.svelte +11 -15
  23. package/dist/components/RectClipPath.svelte.d.ts +6 -0
  24. package/dist/components/Text.svelte +7 -0
  25. package/dist/components/Tree.svelte +7 -3
  26. package/dist/utils/__screenshots__/canvas.svelte.test.ts/renderPathData-composes-element-opacity-with-inherited-globalAlpha--Group-opacity--1.png +0 -0
  27. package/dist/utils/__screenshots__/canvas.svelte.test.ts/renderPathData-composes-element-opacity-with-inherited-globalAlpha--Group-opacity--2.png +0 -0
  28. package/dist/utils/canvas.d.ts +2 -0
  29. package/dist/utils/canvas.js +13 -7
  30. package/dist/utils/canvas.svelte.test.js +55 -0
  31. package/dist/utils/connectorUtils.d.ts +13 -0
  32. package/dist/utils/connectorUtils.js +120 -1
  33. package/dist/utils/path.d.ts +19 -0
  34. package/dist/utils/path.js +72 -0
  35. package/dist/utils/rect.svelte.d.ts +18 -0
  36. package/dist/utils/rect.svelte.js +33 -0
  37. package/package.json +1 -1
@@ -3,20 +3,31 @@
3
3
  import { extractTweenConfig, type MotionProp } from '../utils/motion.svelte.js';
4
4
  import type { SVGAttributes } from 'svelte/elements';
5
5
 
6
+ /** Props forwarded onto the underlying grid line (`<Line>`, `<Circle>`, or `<Spline>`). */
7
+ type GridLineProps = Pick<SVGAttributes<SVGElement>, 'class' | 'style'> & {
8
+ stroke?: string;
9
+ strokeWidth?: number;
10
+ opacity?: number;
11
+ /** Dashed-line pattern. See `Line.dashArray`. */
12
+ dashArray?: number | number[] | string;
13
+ };
14
+
6
15
  export type GridPropsWithoutHTML<In extends Transition = Transition> = {
7
16
  /**
8
- * Draw a x-axis lines
17
+ * Draw a x-axis lines. Pass props (class, style, stroke, strokeWidth,
18
+ * opacity, dashArray) to forward onto the underlying line.
9
19
  *
10
20
  * @default false
11
21
  */
12
- x?: boolean | Pick<SVGAttributes<SVGElement>, 'class' | 'style'>;
22
+ x?: boolean | GridLineProps;
13
23
 
14
24
  /**
15
- * Draw a y-axis lines
25
+ * Draw a y-axis lines. Pass props (class, style, stroke, strokeWidth,
26
+ * opacity, dashArray) to forward onto the underlying line.
16
27
  *
17
28
  * @default false
18
29
  */
19
- y?: boolean | Pick<SVGAttributes<SVGElement>, 'class' | 'style'>;
30
+ y?: boolean | GridLineProps;
20
31
 
21
32
  /**
22
33
  * Control the number of x-axis ticks
@@ -1,19 +1,29 @@
1
1
  import type { Transition, TransitionParams, Without } from '../utils/types.js';
2
2
  import { type MotionProp } from '../utils/motion.svelte.js';
3
3
  import type { SVGAttributes } from 'svelte/elements';
4
+ /** Props forwarded onto the underlying grid line (`<Line>`, `<Circle>`, or `<Spline>`). */
5
+ type GridLineProps = Pick<SVGAttributes<SVGElement>, 'class' | 'style'> & {
6
+ stroke?: string;
7
+ strokeWidth?: number;
8
+ opacity?: number;
9
+ /** Dashed-line pattern. See `Line.dashArray`. */
10
+ dashArray?: number | number[] | string;
11
+ };
4
12
  export type GridPropsWithoutHTML<In extends Transition = Transition> = {
5
13
  /**
6
- * Draw a x-axis lines
14
+ * Draw a x-axis lines. Pass props (class, style, stroke, strokeWidth,
15
+ * opacity, dashArray) to forward onto the underlying line.
7
16
  *
8
17
  * @default false
9
18
  */
10
- x?: boolean | Pick<SVGAttributes<SVGElement>, 'class' | 'style'>;
19
+ x?: boolean | GridLineProps;
11
20
  /**
12
- * Draw a y-axis lines
21
+ * Draw a y-axis lines. Pass props (class, style, stroke, strokeWidth,
22
+ * opacity, dashArray) to forward onto the underlying line.
13
23
  *
14
24
  * @default false
15
25
  */
16
- y?: boolean | Pick<SVGAttributes<SVGElement>, 'class' | 'style'>;
26
+ y?: boolean | GridLineProps;
17
27
  /**
18
28
  * Control the number of x-axis ticks
19
29
  */
@@ -681,6 +681,7 @@
681
681
  y1={line.y1}
682
682
  x2={line.x2}
683
683
  y2={line.y2}
684
+ dashArray={[2, 2]}
684
685
  {opacity}
685
686
  {...extractLayerProps(linesProp, 'lc-highlight-line')}
686
687
  />
@@ -111,6 +111,14 @@
111
111
 
112
112
  /** Motion configuration (pixel mode only). */
113
113
  motion?: MotionProp;
114
+
115
+ /**
116
+ * Dashed-line pattern. Accepts a number (single dash length), a
117
+ * `[dash, gap, ...]` array, or a string (same syntax as SVG
118
+ * `stroke-dasharray`). Works across `<Svg>`, `<Canvas>`, and `<Html>`
119
+ * layers — HTML approximates the pattern via `repeating-linear-gradient`.
120
+ */
121
+ dashArray?: number | number[] | string;
114
122
  } & DataDrivenStyleProps;
115
123
 
116
124
  export type LineProps = LinePropsWithoutHTML &
@@ -137,6 +145,7 @@
137
145
 
138
146
  import { createKey } from '../utils/key.svelte.js';
139
147
  import { createId } from '../utils/createId.js';
148
+ import { parseDashArray, dashArrayToGradient } from '../utils/path.js';
140
149
 
141
150
  const uid = $props.id();
142
151
 
@@ -162,9 +171,13 @@
162
171
  markerMid,
163
172
  motion,
164
173
  fillOpacity,
174
+ dashArray,
165
175
  ...restProps
166
176
  }: LineProps = $props();
167
177
 
178
+ const dashArrayResolved = $derived(parseDashArray(dashArray));
179
+ const dashArrayAttr = $derived(dashArrayResolved ? dashArrayResolved.join(' ') : undefined);
180
+
168
181
  // Data mode detection
169
182
  const dataMode = $derived(hasAnyDataProp(x1, y1, x2, y2));
170
183
 
@@ -285,7 +298,12 @@
285
298
  'lc-line',
286
299
  itemClass ?? (typeof className === 'string' ? className : undefined)
287
300
  ),
288
- style: restProps.style as string | undefined,
301
+ style: [
302
+ restProps.style as string | undefined,
303
+ dashArrayAttr ? `stroke-dasharray: ${dashArrayAttr}` : undefined,
304
+ ]
305
+ .filter(Boolean)
306
+ .join('; ') || undefined,
289
307
  };
290
308
  }
291
309
 
@@ -358,6 +376,7 @@
358
376
  opacity,
359
377
  className,
360
378
  restProps.style,
379
+ dashArrayAttr,
361
380
  ],
362
381
  }
363
382
  : undefined,
@@ -390,6 +409,7 @@
390
409
  marker-start={markerStartId ? `url(#${markerStartId})` : undefined}
391
410
  marker-mid={markerMidId ? `url(#${markerMidId})` : undefined}
392
411
  marker-end={markerEndId ? `url(#${markerEndId})` : undefined}
412
+ stroke-dasharray={dashArrayAttr}
393
413
  class={cls('lc-line', resolvedClass)}
394
414
  {...restProps}
395
415
  />
@@ -408,6 +428,7 @@
408
428
  marker-start={markerStartId ? `url(#${markerStartId})` : undefined}
409
429
  marker-mid={markerMidId ? `url(#${markerMidId})` : undefined}
410
430
  marker-end={markerEndId ? `url(#${markerEndId})` : undefined}
431
+ stroke-dasharray={dashArrayAttr}
411
432
  class={cls('lc-line', staticClassName)}
412
433
  {...restProps}
413
434
  />
@@ -435,7 +456,10 @@
435
456
  style:transform="translateY(-50%) rotate({angle}deg)"
436
457
  style:transform-origin="0 50%"
437
458
  style:opacity={resolvedOpacity}
438
- style:background-color={resolvedStroke}
459
+ style:background={dashArrayResolved
460
+ ? dashArrayToGradient(dashArrayResolved, resolvedStroke ?? 'var(--stroke-color)')
461
+ : undefined}
462
+ style:background-color={dashArrayResolved ? undefined : resolvedStroke}
439
463
  class={cls('lc-line', resolvedClass)}
440
464
  style={restProps.style}
441
465
  ></div>
@@ -455,7 +479,10 @@
455
479
  style:transform="translateY(-50%) rotate({angle}deg)"
456
480
  style:transform-origin="0 50%"
457
481
  style:opacity={staticOpacity}
458
- style:background-color={staticStroke}
482
+ style:background={dashArrayResolved
483
+ ? dashArrayToGradient(dashArrayResolved, staticStroke ?? 'var(--stroke-color)')
484
+ : undefined}
485
+ style:background-color={dashArrayResolved ? undefined : staticStroke}
459
486
  class={cls('lc-line', staticClassName)}
460
487
  style={restProps.style}
461
488
  ></div>
@@ -93,6 +93,13 @@ export type LinePropsWithoutHTML = {
93
93
  markerEnd?: MarkerOptions;
94
94
  /** Motion configuration (pixel mode only). */
95
95
  motion?: MotionProp;
96
+ /**
97
+ * Dashed-line pattern. Accepts a number (single dash length), a
98
+ * `[dash, gap, ...]` array, or a string (same syntax as SVG
99
+ * `stroke-dasharray`). Works across `<Svg>`, `<Canvas>`, and `<Html>`
100
+ * layers — HTML approximates the pattern via `repeating-linear-gradient`.
101
+ */
102
+ dashArray?: number | number[] | string;
96
103
  } & DataDrivenStyleProps;
97
104
  export type LineProps = LinePropsWithoutHTML & Without<SVGAttributes<SVGPathElement>, LinePropsWithoutHTML>;
98
105
  declare const Line: import("svelte").Component<LineProps, {}, "">;
@@ -82,6 +82,9 @@ TODO:
82
82
  */
83
83
  import Connector, { type ConnectorProps } from './Connector.svelte';
84
84
  import { extractLayerProps } from '../utils/attributes.js';
85
+ import { getChartContext } from '../contexts/chart.js';
86
+
87
+ const ctx = getChartContext();
85
88
 
86
89
  let {
87
90
  data,
@@ -95,6 +98,7 @@ TODO:
95
98
  explicitCoords,
96
99
  type = 'd3',
97
100
  sweep = 'none',
101
+ radius = 20,
98
102
  ...restProps
99
103
  }: LinkProps = $props();
100
104
 
@@ -125,12 +129,14 @@ TODO:
125
129
  const xAccessor = $derived.by(() => {
126
130
  if (xProp) return xProp;
127
131
  if (sankey) return (d: any) => (d.isSource ? d.node.x1 : d.node.x0);
132
+ if (ctx.radial) return (d: any) => d.x;
128
133
  return (d: any) => (orientation === 'horizontal' ? d.y : d.x);
129
134
  });
130
135
 
131
136
  const yAccessor = $derived.by(() => {
132
137
  if (yProp) return yProp;
133
138
  if (sankey) return (d: any) => d.y;
139
+ if (ctx.radial) return (d: any) => d.y;
134
140
  return (d: any) => (orientation === 'horizontal' ? d.x : d.y);
135
141
  });
136
142
 
@@ -165,6 +171,7 @@ TODO:
165
171
  return FALLBACK_COORDS;
166
172
  }
167
173
  });
174
+
168
175
  </script>
169
176
 
170
177
  <Connector
@@ -173,5 +180,7 @@ TODO:
173
180
  {type}
174
181
  {curve}
175
182
  {sweep}
183
+ {radius}
184
+ radial={ctx.radial}
176
185
  {...extractLayerProps(restProps, 'lc-link')}
177
186
  />
@@ -5,7 +5,13 @@
5
5
  import { createMotion, parseMotionProp, type MotionProp } from '../utils/motion.svelte.js';
6
6
  import { renderRect, type ComputedStylesOptions } from '../utils/canvas.js';
7
7
  import type { DataProp, DataDrivenStyleProps } from '../utils/dataProp.js';
8
- import type { Insets } from '../utils/rect.svelte.js';
8
+ import {
9
+ resolveCorners,
10
+ cornersUniform,
11
+ type Corners,
12
+ type Insets,
13
+ } from '../utils/rect.svelte.js';
14
+ import { roundedRectPath, parseDashArray } from '../utils/path.js';
9
15
 
10
16
  export type RectPropsWithoutHTML = {
11
17
  /**
@@ -119,6 +125,20 @@
119
125
  /** Motion configuration (pixel mode only). */
120
126
  motion?: MotionProp<'x' | 'y' | 'width' | 'height'>;
121
127
 
128
+ /**
129
+ * Dashed-border pattern. Accepts a number (single dash length), a
130
+ * `[dash, gap, ...]` array, or a string (same syntax as SVG
131
+ * `stroke-dasharray`). HTML layer approximates via `border-style: dashed`.
132
+ */
133
+ dashArray?: number | number[] | string;
134
+
135
+ /**
136
+ * Per-corner radii. Accepts a number (all corners equal — same as `rx`),
137
+ * a `[tl, tr, br, bl]` tuple, or `{ topLeft, topRight, bottomRight, bottomLeft }`.
138
+ * Takes precedence over `rx`/`ry` when corners differ.
139
+ */
140
+ corners?: Corners;
141
+
122
142
  /** Children content to render. Note: Only works for Html layers */
123
143
  children?: Snippet;
124
144
  } & DataDrivenStyleProps;
@@ -175,6 +195,8 @@
175
195
  key: keyFn = (_: any, i: number) => i,
176
196
  ref: refProp = $bindable(),
177
197
  motion,
198
+ corners,
199
+ dashArray,
178
200
  class: className,
179
201
  onclick,
180
202
  ondblclick,
@@ -279,10 +301,26 @@
279
301
  });
280
302
  });
281
303
 
304
+ // Fold a uniform `corners` value into rx/ry so SVG `<rect>` renders rounded
305
+ // corners without needing a `<path>`. When corners are per-corner different,
306
+ // `pixelPathData` kicks in below and SVG emits a `<path>` instead.
307
+ const dashArrayResolved = $derived(parseDashArray(dashArray));
308
+ const dashArrayAttr = $derived(dashArrayResolved ? dashArrayResolved.join(' ') : undefined);
309
+
310
+ const cornersUniformValue = $derived.by(() => {
311
+ if (corners === undefined) return undefined;
312
+ if (typeof corners === 'number') return corners;
313
+ const resolved = resolveCorners(corners, Infinity, Infinity);
314
+ return cornersUniform(resolved) ? resolved[0] : undefined;
315
+ });
316
+ const cornersNonUniform = $derived(
317
+ corners !== undefined && cornersUniformValue === undefined
318
+ );
319
+
282
320
  // Normalize rx/ry - if only one is provided, use it for both (SVG behavior)
283
321
  // Coerce to number for canvas rendering (SVG allows string like "50%")
284
- const rx = $derived(Number(rxProp ?? ryProp) || 0);
285
- const ry = $derived(Number(ryProp ?? rxProp) || 0);
322
+ const rx = $derived(Number(rxProp ?? ryProp ?? cornersUniformValue) || 0);
323
+ const ry = $derived(Number(ryProp ?? rxProp ?? cornersUniformValue) || 0);
286
324
 
287
325
  // --- Pixel mode ---
288
326
  let ref = $state<SVGRectElement>();
@@ -335,6 +373,27 @@
335
373
  );
336
374
  const htmlRestProps = $derived(restProps as unknown as HTMLAttributes<HTMLDivElement>);
337
375
 
376
+ // Resolved per-corner radii for the static (pixel-mode) rect, clamped to bounds.
377
+ const resolvedCorners = $derived(
378
+ corners !== undefined
379
+ ? resolveCorners(corners, motionWidth.current, motionHeight.current)
380
+ : undefined
381
+ );
382
+ const borderRadiusStyle = $derived(
383
+ resolvedCorners ? resolvedCorners.map((c) => `${c}px`).join(' ') : undefined
384
+ );
385
+ const pixelPathData = $derived(
386
+ resolvedCorners && cornersNonUniform
387
+ ? roundedRectPath(
388
+ motionX.current,
389
+ motionY.current,
390
+ motionWidth.current,
391
+ motionHeight.current,
392
+ resolvedCorners
393
+ )
394
+ : undefined
395
+ );
396
+
338
397
  function getStyleOptions(
339
398
  styleOverrides: ComputedStylesOptions | undefined,
340
399
  itemFill?: string | undefined,
@@ -371,7 +430,12 @@
371
430
  'lc-rect',
372
431
  itemClass ?? (typeof className === 'string' ? className : undefined)
373
432
  ),
374
- style: restProps.style as string | undefined,
433
+ style: [
434
+ restProps.style as string | undefined,
435
+ dashArrayAttr ? `stroke-dasharray: ${dashArrayAttr}` : undefined,
436
+ ]
437
+ .filter(Boolean)
438
+ .join('; ') || undefined,
375
439
  };
376
440
  }
377
441
 
@@ -422,6 +486,7 @@
422
486
  height: motionHeight.current,
423
487
  rx,
424
488
  ry,
489
+ corners: resolvedCorners,
425
490
  },
426
491
  styleOpts
427
492
  );
@@ -475,6 +540,8 @@
475
540
  restProps.style,
476
541
  rx,
477
542
  ry,
543
+ resolvedCorners,
544
+ dashArrayAttr,
478
545
  ],
479
546
  }
480
547
  : undefined,
@@ -504,6 +571,7 @@
504
571
  opacity={resolvedOpacity}
505
572
  {rx}
506
573
  {ry}
574
+ stroke-dasharray={dashArrayAttr}
507
575
  class={cls('lc-rect', resolvedClass)}
508
576
  {...restProps}
509
577
  {onclick}
@@ -515,6 +583,28 @@
515
583
  {onpointerout}
516
584
  />
517
585
  {/each}
586
+ {:else if pixelPathData}
587
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
588
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
589
+ <path
590
+ d={pixelPathData}
591
+ fill={staticFill}
592
+ fill-opacity={staticFillOpacity}
593
+ stroke={staticStroke}
594
+ stroke-opacity={staticStrokeOpacity}
595
+ stroke-width={staticStrokeWidth}
596
+ opacity={staticOpacity}
597
+ stroke-dasharray={dashArrayAttr}
598
+ class={cls('lc-rect', staticClassName)}
599
+ {...(restProps as unknown as SVGAttributes<SVGPathElement>)}
600
+ {onclick}
601
+ {ondblclick}
602
+ {onpointerenter}
603
+ {onpointermove}
604
+ {onpointerleave}
605
+ {onpointerover}
606
+ {onpointerout}
607
+ />
518
608
  {:else}
519
609
  <rect
520
610
  x={motionX.current}
@@ -529,6 +619,7 @@
529
619
  opacity={staticOpacity}
530
620
  {rx}
531
621
  {ry}
622
+ stroke-dasharray={dashArrayAttr}
532
623
  class={cls('lc-rect', staticClassName)}
533
624
  {...restProps}
534
625
  {onclick}
@@ -561,7 +652,7 @@
561
652
  style:background={resolvedFill}
562
653
  style:opacity={resolvedOpacity}
563
654
  style:border-width="{resolvedStrokeWidth}px"
564
- style:border-style="solid"
655
+ style:border-style={dashArrayResolved ? 'dashed' : 'solid'}
565
656
  style:border-color={resolvedStroke}
566
657
  style:border-radius="{rx}px"
567
658
  class={cls('lc-rect', resolvedClass)}
@@ -587,9 +678,9 @@
587
678
  style:background={staticFill}
588
679
  style:opacity={staticOpacity}
589
680
  style:border-width={staticBorderWidth}
590
- style:border-style="solid"
681
+ style:border-style={dashArrayResolved ? 'dashed' : 'solid'}
591
682
  style:border-color={staticStroke}
592
- style:border-radius="{rx}px"
683
+ style:border-radius={borderRadiusStyle ?? `${rx}px`}
593
684
  class={cls('lc-rect', staticClassName)}
594
685
  {...htmlRestProps}
595
686
  {onclick}
@@ -3,7 +3,7 @@ import type { SVGAttributes } from 'svelte/elements';
3
3
  import type { CommonEvents, Without } from '../utils/types.js';
4
4
  import { type MotionProp } from '../utils/motion.svelte.js';
5
5
  import type { DataProp, DataDrivenStyleProps } from '../utils/dataProp.js';
6
- import type { Insets } from '../utils/rect.svelte.js';
6
+ import { type Corners, type Insets } from '../utils/rect.svelte.js';
7
7
  export type RectPropsWithoutHTML = {
8
8
  /**
9
9
  * The x position of the rectangle.
@@ -101,6 +101,18 @@ export type RectPropsWithoutHTML = {
101
101
  ref?: SVGRectElement;
102
102
  /** Motion configuration (pixel mode only). */
103
103
  motion?: MotionProp<'x' | 'y' | 'width' | 'height'>;
104
+ /**
105
+ * Dashed-border pattern. Accepts a number (single dash length), a
106
+ * `[dash, gap, ...]` array, or a string (same syntax as SVG
107
+ * `stroke-dasharray`). HTML layer approximates via `border-style: dashed`.
108
+ */
109
+ dashArray?: number | number[] | string;
110
+ /**
111
+ * Per-corner radii. Accepts a number (all corners equal — same as `rx`),
112
+ * a `[tl, tr, br, bl]` tuple, or `{ topLeft, topRight, bottomRight, bottomLeft }`.
113
+ * Takes precedence over `rx`/`ry` when corners differ.
114
+ */
115
+ corners?: Corners;
104
116
  /** Children content to render. Note: Only works for Html layers */
105
117
  children?: Snippet;
106
118
  } & DataDrivenStyleProps;
@@ -1,5 +1,5 @@
1
1
  <script lang="ts" module>
2
- import Rect, { type RectPropsWithoutHTML } from './Rect.svelte';
2
+ import { type RectPropsWithoutHTML } from './Rect.svelte';
3
3
  import type { CommonEvents, Without } from '../utils/types.js';
4
4
  import type { SVGAttributes } from 'svelte/elements';
5
5
  import type { Snippet } from 'svelte';
@@ -45,6 +45,13 @@
45
45
  */
46
46
  disabled?: boolean;
47
47
 
48
+ /**
49
+ * Invert the clip — content renders *outside* the rect.
50
+ *
51
+ * @default false
52
+ */
53
+ invert?: boolean;
54
+
48
55
  /**
49
56
  * The default children snippet which provides
50
57
  * the id and url for the clipPath.
@@ -65,7 +72,6 @@
65
72
  <script lang="ts">
66
73
  import ClipPath from './ClipPath.svelte';
67
74
  import { createId } from '../utils/createId.js';
68
- import { extractLayerProps } from '../utils/attributes.js';
69
75
  import type { MotionProp } from '../utils/motion.svelte.js';
70
76
 
71
77
  const uid = $props.id();
@@ -77,24 +83,14 @@
77
83
  width,
78
84
  height,
79
85
  disabled = false,
86
+ invert = false,
80
87
  children: childrenProp,
81
- ...restProps
82
88
  }: RectClipPathProps = $props();
83
89
 
84
- function canvasClip(ctx: CanvasRenderingContext2D) {
85
- ctx.beginPath();
86
- ctx.rect(x, y, width, height);
87
- }
88
-
89
- function canvasClipDeps() {
90
- return [x, y, width, height];
91
- }
90
+ const path = $derived(`M${x},${y} h${width} v${height} h${-width} Z`);
92
91
  </script>
93
92
 
94
- <ClipPath {id} {disabled} {canvasClip} {canvasClipDeps}>
95
- {#snippet clip()}
96
- <Rect {x} {y} {width} {height} {...extractLayerProps(restProps, 'lc-clip-path-rect')} />
97
- {/snippet}
93
+ <ClipPath {id} {disabled} {invert} {path}>
98
94
  {#snippet children({ url })}
99
95
  {@render childrenProp?.({ id, url })}
100
96
  {/snippet}
@@ -37,6 +37,12 @@ export type BaseRectClipPathPropsWithoutHTML = {
37
37
  * @default false
38
38
  */
39
39
  disabled?: boolean;
40
+ /**
41
+ * Invert the clip — content renders *outside* the rect.
42
+ *
43
+ * @default false
44
+ */
45
+ invert?: boolean;
40
46
  /**
41
47
  * The default children snippet which provides
42
48
  * the id and url for the clipPath.
@@ -864,6 +864,9 @@
864
864
  {#if dataMode}
865
865
  {#each resolvedItems as item (item.key)}
866
866
  {@const text = resolveTextValue(item.d)}
867
+ {@const resolvedFill = resolveColorProp(fill, item.d, chartCtx.cScale)}
868
+ {@const resolvedFillOpacity = resolveStyleProp(fillOpacity, item.d)}
869
+ {@const resolvedOpacity = resolveStyleProp(opacity, item.d)}
867
870
  {@const resolvedClass = resolveStyleProp(className, item.d)}
868
871
  {@const translateX = textAnchor === 'middle' ? '-50%' : textAnchor === 'end' ? '-100%' : '0%'}
869
872
  {@const translateY =
@@ -881,6 +884,8 @@
881
884
  {textAnchor === 'middle' ? 'center' : textAnchor === 'end' ? 'right' : 'left'}"
882
885
  style:white-space="pre-wrap"
883
886
  style:line-height={lineHeight}
887
+ style:color={resolvedFill}
888
+ style:opacity={resolvedOpacity ?? resolvedFillOpacity}
884
889
  class={['lc-text', resolvedClass]}
885
890
  >
886
891
  {text}
@@ -903,6 +908,8 @@
903
908
  {textAnchor === 'middle' ? 'center' : textAnchor === 'end' ? 'right' : 'left'}"
904
909
  style:white-space="pre-wrap"
905
910
  style:line-height={lineHeight}
911
+ style:color={staticFill}
912
+ style:opacity={staticOpacity ?? staticFillOpacity}
906
913
  class={['lc-text', staticClassName]}
907
914
  >
908
915
  {#if segments}
@@ -49,9 +49,13 @@
49
49
  const ctx = getChartContext();
50
50
 
51
51
  const treeData = $derived.by(() => {
52
- const _tree = d3Tree<T>().size(
53
- orientation === 'horizontal' ? [ctx.height, ctx.width] : [ctx.width, ctx.height]
54
- );
52
+ const _tree = ctx.radial
53
+ ? d3Tree<T>()
54
+ .size([2 * Math.PI, Math.min(ctx.width, ctx.height) / 2])
55
+ .separation((a, b) => (a.parent === b.parent ? 1 : 2) / a.depth)
56
+ : d3Tree<T>().size(
57
+ orientation === 'horizontal' ? [ctx.height, ctx.width] : [ctx.width, ctx.height]
58
+ );
55
59
 
56
60
  if (nodeSize) {
57
61
  _tree.nodeSize(nodeSize);
@@ -32,6 +32,8 @@ export declare function renderRect(ctx: CanvasRenderingContext2D, coords: {
32
32
  height: number;
33
33
  rx?: number;
34
34
  ry?: number;
35
+ /** Per-corner radii [tl, tr, br, bl]. Takes precedence over `rx`/`ry`. */
36
+ corners?: [number, number, number, number];
35
37
  }, styleOptions?: ComputedStylesOptions): void;
36
38
  export declare function renderCircle(ctx: CanvasRenderingContext2D, coords: {
37
39
  cx: number;
@@ -1,5 +1,6 @@
1
1
  import memoize from 'memoize';
2
2
  import { cls } from '@layerstack/tailwind';
3
+ import { roundedRectPath } from './path.js';
3
4
  /** @deprecated - use `isTransparentFill` instead */
4
5
  export const DEFAULT_FILL = 'rgb(0, 0, 0)';
5
6
  /**
@@ -152,7 +153,7 @@ function render(ctx, render, styleOptions = {}, { applyText, } = {}) {
152
153
  // Adhere to CSS paint order: https://developer.mozilla.org/en-US/docs/Web/CSS/paint-order
153
154
  const paintOrder = resolvedStyles?.paintOrder === 'stroke' ? ['stroke', 'fill'] : ['fill', 'stroke'];
154
155
  if (resolvedStyles?.opacity) {
155
- ctx.globalAlpha = Number(resolvedStyles?.opacity);
156
+ ctx.globalAlpha *= Number(resolvedStyles?.opacity);
156
157
  }
157
158
  // font/text properties can be expensive to set (not sure why), so only apply if needed (renderText())
158
159
  if (applyText) {
@@ -203,8 +204,7 @@ function render(ctx, render, styleOptions = {}, { applyText, } = {}) {
203
204
  if (fill && !isTransparentFill(fill)) {
204
205
  const currentGlobalAlpha = ctx.globalAlpha;
205
206
  const fillOpacity = Number(resolvedStyles?.fillOpacity);
206
- const opacity = Number(resolvedStyles?.opacity);
207
- ctx.globalAlpha = fillOpacity * opacity;
207
+ ctx.globalAlpha *= isNaN(fillOpacity) ? 1 : fillOpacity;
208
208
  ctx.fillStyle = fill;
209
209
  render.fill(ctx);
210
210
  // Restore in case it was modified by `fillOpacity`
@@ -222,7 +222,7 @@ function render(ctx, render, styleOptions = {}, { applyText, } = {}) {
222
222
  const strokeOpacity = Number(resolvedStyles?.strokeOpacity);
223
223
  const opacity = Number(resolvedStyles?.opacity);
224
224
  if (!isNaN(strokeOpacity) && strokeOpacity !== 1) {
225
- ctx.globalAlpha = strokeOpacity * (isNaN(opacity) ? 1 : opacity);
225
+ ctx.globalAlpha *= strokeOpacity;
226
226
  }
227
227
  ctx.lineWidth =
228
228
  typeof resolvedStyles?.strokeWidth === 'string'
@@ -253,11 +253,12 @@ export function renderText(ctx, text, coords, styleOptions = {}) {
253
253
  }
254
254
  }
255
255
  export function renderRect(ctx, coords, styleOptions = {}) {
256
- const { x, y, width, height } = coords;
256
+ const { x, y, width, height, corners } = coords;
257
257
  const rx = coords.rx ?? 0;
258
258
  const ry = coords.ry ?? rx; // Default ry to rx if not provided (SVG behavior)
259
+ const perCorner = corners && !corners.every((c) => c === corners[0]);
259
260
  // No rounding - use simple rect methods
260
- if (rx === 0 && ry === 0) {
261
+ if (!perCorner && rx === 0 && ry === 0 && !corners) {
261
262
  render(ctx, {
262
263
  fill: (ctx) => ctx.fillRect(x, y, width, height),
263
264
  stroke: (ctx) => ctx.strokeRect(x, y, width, height),
@@ -267,7 +268,7 @@ export function renderRect(ctx, coords, styleOptions = {}) {
267
268
  // Try native roundRect if available (modern browsers)
268
269
  if (typeof ctx.roundRect === 'function') {
269
270
  ctx.beginPath();
270
- ctx.roundRect(x, y, width, height, [rx, ry]);
271
+ ctx.roundRect(x, y, width, height, corners ?? [rx, ry]);
271
272
  render(ctx, {
272
273
  fill: (ctx) => ctx.fill(),
273
274
  stroke: (ctx) => ctx.stroke(),
@@ -276,6 +277,11 @@ export function renderRect(ctx, coords, styleOptions = {}) {
276
277
  return;
277
278
  }
278
279
  // Fallback: use path rendering for rounded corners
280
+ if (corners) {
281
+ const pathData = roundedRectPath(x, y, width, height, corners);
282
+ renderPathData(ctx, pathData, styleOptions);
283
+ return;
284
+ }
279
285
  // Clamp radii to half the width/height
280
286
  const clampedRx = Math.min(rx, width / 2);
281
287
  const clampedRy = Math.min(ry, height / 2);