layerchart 2.0.0-next.54 → 2.0.0-next.56

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 (81) hide show
  1. package/dist/bench/ComposableLineChart.svelte +1 -1
  2. package/dist/bench/GeoBench.svelte +1 -8
  3. package/dist/components/AnnotationRange.svelte +3 -1
  4. package/dist/components/Arc.svelte +1 -3
  5. package/dist/components/ArcLabel.svelte.test.js +7 -7
  6. package/dist/components/Axis.svelte +10 -2
  7. package/dist/components/Axis.svelte.d.ts +8 -2
  8. package/dist/components/Bar.svelte +14 -40
  9. package/dist/components/BoxPlot.svelte +4 -12
  10. package/dist/components/Cell.svelte +13 -8
  11. package/dist/components/Chart.svelte +69 -26
  12. package/dist/components/ChartChildren.svelte +22 -4
  13. package/dist/components/Circle.svelte +51 -9
  14. package/dist/components/Circle.svelte.d.ts +6 -0
  15. package/dist/components/CircleClipPath.svelte +13 -31
  16. package/dist/components/CircleClipPath.svelte.d.ts +7 -1
  17. package/dist/components/ClipPath.svelte +58 -21
  18. package/dist/components/ClipPath.svelte.d.ts +21 -12
  19. package/dist/components/Connector.svelte +18 -0
  20. package/dist/components/Connector.svelte.d.ts +5 -0
  21. package/dist/components/Ellipse.svelte +27 -6
  22. package/dist/components/GeoClipPath.svelte +14 -17
  23. package/dist/components/GeoClipPath.svelte.d.ts +6 -0
  24. package/dist/components/GeoLegend.svelte +1 -3
  25. package/dist/components/GeoPoint.svelte +25 -3
  26. package/dist/components/GeoSpline.svelte +1 -4
  27. package/dist/components/GeoTile.svelte +8 -4
  28. package/dist/components/Grid.svelte +15 -4
  29. package/dist/components/Grid.svelte.d.ts +14 -4
  30. package/dist/components/Group.svelte +11 -5
  31. package/dist/components/Highlight.svelte +4 -3
  32. package/dist/components/Image.svelte +42 -30
  33. package/dist/components/Labels.svelte +2 -4
  34. package/dist/components/Line.svelte +31 -3
  35. package/dist/components/Line.svelte.d.ts +7 -0
  36. package/dist/components/LinearGradient.svelte +8 -4
  37. package/dist/components/Link.svelte +8 -0
  38. package/dist/components/Marker.svelte +9 -1
  39. package/dist/components/Path.svelte +43 -23
  40. package/dist/components/Pattern.svelte +101 -5
  41. package/dist/components/Pattern.svelte.d.ts +3 -1
  42. package/dist/components/Pie.svelte +2 -6
  43. package/dist/components/RadialGradient.svelte +8 -4
  44. package/dist/components/Rect.svelte +117 -9
  45. package/dist/components/Rect.svelte.d.ts +13 -1
  46. package/dist/components/RectClipPath.svelte +11 -15
  47. package/dist/components/RectClipPath.svelte.d.ts +6 -0
  48. package/dist/components/Spline.svelte +22 -4
  49. package/dist/components/Text.svelte +16 -5
  50. package/dist/components/Trail.svelte +19 -7
  51. package/dist/components/Tree.svelte +7 -3
  52. package/dist/components/Vector.svelte +37 -14
  53. package/dist/components/Violin.svelte +1 -2
  54. package/dist/components/charts/ArcChart.svelte +8 -5
  55. package/dist/components/charts/AreaChart.svelte +6 -1
  56. package/dist/components/charts/BarChart.svelte +3 -1
  57. package/dist/components/charts/LineChart.svelte +6 -1
  58. package/dist/components/charts/PieChart.svelte +10 -3
  59. package/dist/components/tooltip/Tooltip.svelte +2 -8
  60. package/dist/contexts/chart.d.ts +1 -1
  61. package/dist/contexts/chart.js +3 -1
  62. package/dist/server/TestBarChart.svelte +28 -28
  63. package/dist/server/TestLineChart.svelte +28 -28
  64. package/dist/server/index.js +1 -1
  65. package/dist/states/brush.svelte.js +16 -13
  66. package/dist/states/chart.svelte.test.js +24 -19
  67. package/dist/states/geo.svelte.js +1 -4
  68. package/dist/states/series.svelte.js +1 -1
  69. package/dist/utils/__screenshots__/canvas.svelte.test.ts/renderPathData-composes-element-opacity-with-inherited-globalAlpha--Group-opacity--1.png +0 -0
  70. package/dist/utils/__screenshots__/canvas.svelte.test.ts/renderPathData-composes-element-opacity-with-inherited-globalAlpha--Group-opacity--2.png +0 -0
  71. package/dist/utils/canvas.d.ts +2 -0
  72. package/dist/utils/canvas.js +20 -11
  73. package/dist/utils/canvas.svelte.test.js +55 -0
  74. package/dist/utils/connectorUtils.d.ts +13 -0
  75. package/dist/utils/connectorUtils.js +120 -1
  76. package/dist/utils/path.d.ts +19 -0
  77. package/dist/utils/path.js +72 -0
  78. package/dist/utils/rect.svelte.d.ts +18 -0
  79. package/dist/utils/rect.svelte.js +33 -0
  80. package/dist/utils/trail.js +3 -4
  81. package/package.json +1 -1
@@ -78,6 +78,13 @@
78
78
  /** Motion configuration (pixel mode only). */
79
79
  motion?: MotionProp;
80
80
 
81
+ /**
82
+ * Dashed-border pattern. Accepts a number (single dash length), a
83
+ * `[dash, gap, ...]` array, or a string (same syntax as SVG
84
+ * `stroke-dasharray`). HTML layer approximates via `border-style: dashed`.
85
+ */
86
+ dashArray?: number | number[] | string;
87
+
81
88
  /** Children content to render. Note: Only works for Html layers */
82
89
  children?: Snippet;
83
90
  } & DataDrivenStyleProps;
@@ -106,6 +113,7 @@
106
113
  import { chartDataArray } from '../utils/common.js';
107
114
  import type { SVGAttributes } from 'svelte/elements';
108
115
  import { createKey } from '../utils/key.svelte.js';
116
+ import { parseDashArray } from '../utils/path.js';
109
117
 
110
118
  let {
111
119
  cx = 0,
@@ -124,10 +132,14 @@
124
132
  opacity,
125
133
  class: className,
126
134
  ref: refProp = $bindable(),
135
+ dashArray,
127
136
  children,
128
137
  ...restProps
129
138
  }: CircleProps = $props();
130
139
 
140
+ const dashArrayResolved = $derived(parseDashArray(dashArray));
141
+ const dashArrayAttr = $derived(dashArrayResolved ? dashArrayResolved.join(' ') : undefined);
142
+
131
143
  // Data mode detection: if any positional prop is a string or function
132
144
  const dataMode = $derived(hasAnyDataProp(cx, cy, r));
133
145
 
@@ -215,6 +227,15 @@
215
227
  const staticStrokeWidth = $derived(typeof strokeWidth === 'number' ? strokeWidth : undefined);
216
228
  const staticOpacity = $derived(typeof opacity === 'number' ? opacity : undefined);
217
229
  const staticClassName = $derived(typeof className === 'string' ? className : undefined);
230
+ // Match SVG's implicit `stroke-width: 1` default: if `stroke` is set but
231
+ // `strokeWidth` is not, render a 1px border so HTML matches SVG/Canvas layers.
232
+ const staticBorderWidth = $derived(
233
+ typeof strokeWidth === 'number'
234
+ ? `${strokeWidth}px`
235
+ : typeof stroke === 'string'
236
+ ? '1px'
237
+ : undefined
238
+ );
218
239
 
219
240
  // Style options (shared between pixel and data mode)
220
241
  function getStyleOptions(
@@ -250,7 +271,13 @@
250
271
  'lc-circle',
251
272
  itemClass ?? (typeof className === 'string' ? className : undefined)
252
273
  ),
253
- style: restProps.style as string | undefined,
274
+ style:
275
+ [
276
+ restProps.style as string | undefined,
277
+ dashArrayAttr ? `stroke-dasharray: ${dashArrayAttr}` : undefined,
278
+ ]
279
+ .filter(Boolean)
280
+ .join('; ') || undefined,
254
281
  };
255
282
  }
256
283
 
@@ -328,6 +355,7 @@
328
355
  opacity,
329
356
  className,
330
357
  restProps.style,
358
+ dashArrayAttr,
331
359
  ],
332
360
  }
333
361
  : undefined,
@@ -352,6 +380,7 @@
352
380
  stroke={resolvedStroke}
353
381
  stroke-width={resolvedStrokeWidth}
354
382
  opacity={resolvedOpacity}
383
+ stroke-dasharray={dashArrayAttr}
355
384
  class={cls('lc-circle', resolvedClass)}
356
385
  {...restProps}
357
386
  />
@@ -367,6 +396,7 @@
367
396
  stroke={staticStroke}
368
397
  stroke-width={staticStrokeWidth}
369
398
  opacity={staticOpacity}
399
+ stroke-dasharray={dashArrayAttr}
370
400
  class={cls('lc-circle', staticClassName)}
371
401
  {...restProps}
372
402
  />
@@ -380,6 +410,12 @@
380
410
  {@const resolvedStrokeWidth = resolveStyleProp(strokeWidth, item.d)}
381
411
  {@const resolvedOpacity = resolveStyleProp(opacity, item.d)}
382
412
  {@const resolvedClass = resolveStyleProp(className, item.d)}
413
+ {@const resolvedBorderWidth =
414
+ resolvedStrokeWidth != null
415
+ ? `${resolvedStrokeWidth}px`
416
+ : resolvedStroke != null
417
+ ? '1px'
418
+ : undefined}
383
419
  <div
384
420
  style:position="absolute"
385
421
  style:left="{item.cx}px"
@@ -387,11 +423,12 @@
387
423
  style:width="{item.r * 2}px"
388
424
  style:height="{item.r * 2}px"
389
425
  style:border-radius="50%"
390
- style:background-color={resolvedFill}
426
+ style:background={resolvedFill}
427
+ style:background-origin="border-box"
391
428
  style:opacity={resolvedOpacity}
392
- style:border-width={resolvedStrokeWidth}
429
+ style:border-width={resolvedBorderWidth}
393
430
  style:border-color={resolvedStroke}
394
- style:border-style="solid"
431
+ style:border-style={dashArrayResolved ? 'dashed' : 'solid'}
395
432
  style:transform="translate(-50%, -50%)"
396
433
  class={cls('lc-circle', resolvedClass)}
397
434
  {...restProps}
@@ -405,11 +442,12 @@
405
442
  style:width="{motionR.current * 2}px"
406
443
  style:height="{motionR.current * 2}px"
407
444
  style:border-radius="50%"
408
- style:background-color={staticFill}
445
+ style:background={staticFill}
446
+ style:background-origin="border-box"
409
447
  style:opacity={staticOpacity}
410
- style:border-width={staticStrokeWidth}
448
+ style:border-width={staticBorderWidth}
411
449
  style:border-color={staticStroke}
412
- style:border-style="solid"
450
+ style:border-style={dashArrayResolved ? 'dashed' : 'solid'}
413
451
  style:transform="translate(-50%, -50%)"
414
452
  class={cls('lc-circle', staticClassName)}
415
453
  {...restProps}
@@ -435,8 +473,12 @@
435
473
  }
436
474
 
437
475
  /* Html layers */
438
- :global(:where(.lc-layout-html .lc-circle):not([background-color])) {
439
- background-color: var(--fill-color);
476
+ :global(:where(.lc-layout-html .lc-circle)) {
477
+ /* Match SVG sizing (visual extent equals `r * 2`, border on outer edge) */
478
+ box-sizing: border-box;
479
+ }
480
+ :global(:where(.lc-layout-html .lc-circle):not([background])) {
481
+ background: var(--fill-color);
440
482
  }
441
483
  :global(:where(.lc-layout-html .lc-circle):not([border-color])) {
442
484
  border-color: var(--stroke-color);
@@ -66,6 +66,12 @@ export type CirclePropsWithoutHTML = {
66
66
  ref?: SVGCircleElement;
67
67
  /** Motion configuration (pixel mode only). */
68
68
  motion?: MotionProp;
69
+ /**
70
+ * Dashed-border pattern. Accepts a number (single dash length), a
71
+ * `[dash, gap, ...]` array, or a string (same syntax as SVG
72
+ * `stroke-dasharray`). HTML layer approximates via `border-style: dashed`.
73
+ */
74
+ dashArray?: number | number[] | string;
69
75
  /** Children content to render. Note: Only works for Html layers */
70
76
  children?: Snippet;
71
77
  } & DataDrivenStyleProps;
@@ -36,6 +36,13 @@
36
36
  */
37
37
  disabled?: boolean;
38
38
 
39
+ /**
40
+ * Invert the clip — content renders *outside* the circle.
41
+ *
42
+ * @default false
43
+ */
44
+ invert?: boolean;
45
+
39
46
  /**
40
47
  * A bindable reference to the underlying `<circle>` element'
41
48
  *
@@ -53,9 +60,7 @@
53
60
  </script>
54
61
 
55
62
  <script lang="ts">
56
- import Circle from './Circle.svelte';
57
63
  import { createId } from '../utils/createId.js';
58
- import { extractLayerProps } from '../utils/attributes.js';
59
64
 
60
65
  const uid = $props.id();
61
66
 
@@ -64,38 +69,15 @@
64
69
  cx = 0,
65
70
  cy = 0,
66
71
  r,
67
- motion,
68
72
  disabled = false,
69
- ref: refProp = $bindable(),
73
+ invert = false,
70
74
  children,
71
- ...restProps
72
75
  }: CircleClipPathPropsWithoutHTML = $props();
73
76
 
74
- let ref = $state<SVGCircleElement>();
75
-
76
- $effect.pre(() => {
77
- refProp = ref;
78
- });
79
-
80
- function canvasClip(ctx: CanvasRenderingContext2D) {
81
- ctx.beginPath();
82
- ctx.arc(cx, cy, r, 0, Math.PI * 2);
83
- }
84
-
85
- function canvasClipDeps() {
86
- return [cx, cy, r];
87
- }
77
+ // Two 180° arcs produce a full circle that Path2D / `clip-path: path()` accept.
78
+ const path = $derived(
79
+ `M${cx - r},${cy} a${r},${r} 0 1,0 ${2 * r},0 a${r},${r} 0 1,0 ${-2 * r},0 Z`
80
+ );
88
81
  </script>
89
82
 
90
- <ClipPath {id} {disabled} {children} {canvasClip} {canvasClipDeps}>
91
- {#snippet clip()}
92
- <Circle
93
- {cx}
94
- {cy}
95
- {r}
96
- {motion}
97
- {...extractLayerProps(restProps, 'lc-clip-path-circle')}
98
- bind:ref
99
- />
100
- {/snippet}
101
- </ClipPath>
83
+ <ClipPath {id} {disabled} {invert} {children} {path} />
@@ -29,6 +29,12 @@ export type CircleClipPathPropsWithoutHTML = {
29
29
  * @default false
30
30
  */
31
31
  disabled?: boolean;
32
+ /**
33
+ * Invert the clip — content renders *outside* the circle.
34
+ *
35
+ * @default false
36
+ */
37
+ invert?: boolean;
32
38
  /**
33
39
  * A bindable reference to the underlying `<circle>` element'
34
40
  *
@@ -41,6 +47,6 @@ export type CircleClipPathPropsWithoutHTML = {
41
47
  children?: ClipPathPropsWithoutHTML['children'];
42
48
  motion?: MotionProp;
43
49
  };
44
- declare const CircleClipPath: import("svelte").Component<CircleClipPathPropsWithoutHTML, {}, "ref">;
50
+ declare const CircleClipPath: import("svelte").Component<CircleClipPathPropsWithoutHTML, {}, "">;
45
51
  type CircleClipPath = ReturnType<typeof CircleClipPath>;
46
52
  export default CircleClipPath;
@@ -26,26 +26,37 @@
26
26
  disabled?: boolean;
27
27
 
28
28
  /**
29
- * A snippet to insert content into the clipPath.
30
- * Provides the id for the clipPath as a snippet prop.
29
+ * Invert the clip content renders *outside* the shape instead of inside.
30
+ * Implemented by combining the shape with an outer rect covering the chart
31
+ * bounds and applying the even-odd fill rule.
32
+ *
33
+ * @default false
31
34
  */
32
- clip?: Snippet<[{ id: string }]>;
35
+ invert?: boolean;
33
36
 
34
37
  /**
35
- * Children to render in the `<g>` element that links to the clipPath (if not disabled).
36
- * Provides the id, url, and useId for the clipPath as snippet props.
38
+ * SVG path `d` string describing the clip shape. When provided, this single
39
+ * value drives all three layers:
40
+ * - SVG: rendered as `<path d={path}>` inside the `<clipPath>`
41
+ * - Canvas: wrapped in `Path2D` and applied via `ctx.clip(...)`
42
+ * - HTML: emitted as CSS `clip-path: path("...")` on a wrapper `<div>`
43
+ *
44
+ * For shapes that can't be expressed as an SVG path (or for advanced
45
+ * per-layer customization), use the `clip` snippet (SVG) alongside `path`.
37
46
  */
38
- children?: Snippet<[{ id: string; url: string; useId?: string }]>;
47
+ path?: string;
48
+
39
49
  /**
40
- * Canvas clip path function. When provided and in canvas mode, sets up a canvas
41
- * clip region by drawing a path and calling `ctx.clip()` before rendering children.
50
+ * A snippet to insert custom SVG content into the `<clipPath>`. When
51
+ * omitted and `path` is set, a `<path d={path}>` is rendered automatically.
42
52
  */
43
- canvasClip?: (ctx: CanvasRenderingContext2D) => void;
53
+ clip?: Snippet<[{ id: string }]>;
54
+
44
55
  /**
45
- * Reactive deps for canvas clip invalidation. Return array of values that,
46
- * when changed, should trigger a canvas redraw.
56
+ * Children to render in the `<g>` element that links to the clipPath (if not disabled).
57
+ * Provides the id, url, and useId for the clipPath as snippet props.
47
58
  */
48
- canvasClipDeps?: () => any[];
59
+ children?: Snippet<[{ id: string; url: string; useId?: string }]>;
49
60
  };
50
61
 
51
62
  export type ClipPathProps = ClipPathPropsWithoutHTML &
@@ -61,10 +72,10 @@
61
72
  id = createId('clipPath-', uid),
62
73
  useId,
63
74
  disabled = false,
75
+ invert = false,
64
76
  children,
65
77
  clip,
66
- canvasClip,
67
- canvasClipDeps,
78
+ path,
68
79
  ...restProps
69
80
  }: ClipPathPropsWithoutHTML = $props();
70
81
 
@@ -73,18 +84,29 @@
73
84
  const layerCtx = getLayerContext();
74
85
  const chartCtx = getChartContext();
75
86
 
87
+ // Outer rect covering the chart bounds — combined with the clip shape under
88
+ // the even-odd fill rule to invert the clip.
89
+ const outerRect = $derived(`M0,0 H${chartCtx.width} V${chartCtx.height} H0 Z`);
90
+
91
+ // Effective path used for canvas + html layers when inverting.
92
+ const effectivePath = $derived(invert && path ? `${outerRect} ${path}` : path);
93
+
94
+ // Cache the Path2D so `ctx.clip()` gets a stable reference per `path` change.
95
+ const canvasPath = $derived(
96
+ layerCtx === 'canvas' && effectivePath ? new Path2D(effectivePath) : undefined
97
+ );
98
+
76
99
  if (layerCtx === 'canvas') {
77
100
  chartCtx.registerComponent({
78
101
  name: 'ClipPath',
79
102
  kind: 'group',
80
103
  canvasRender: {
81
104
  render: (ctx) => {
82
- if (!disabled && canvasClip) {
83
- canvasClip(ctx);
84
- ctx.clip();
105
+ if (!disabled && canvasPath) {
106
+ ctx.clip(canvasPath, invert ? 'evenodd' : 'nonzero');
85
107
  }
86
108
  },
87
- deps: () => [disabled, ...(canvasClipDeps?.() ?? [])],
109
+ deps: () => [disabled, canvasPath, invert],
88
110
  },
89
111
  });
90
112
  }
@@ -93,7 +115,11 @@
93
115
  {#if layerCtx === 'svg'}
94
116
  <defs>
95
117
  <clipPath {id} {...restProps}>
96
- {@render clip?.({ id })}
118
+ {#if clip}
119
+ {@render clip({ id })}
120
+ {:else if effectivePath}
121
+ <path d={effectivePath} clip-rule={invert ? 'evenodd' : undefined} />
122
+ {/if}
97
123
 
98
124
  {#if useId}
99
125
  <use href="#{useId}" />
@@ -103,11 +129,22 @@
103
129
  {/if}
104
130
 
105
131
  {#if children}
106
- {#if disabled || layerCtx !== 'svg'}
132
+ {#if disabled}
107
133
  {@render children({ id, url, useId })}
108
- {:else}
134
+ {:else if layerCtx === 'svg'}
109
135
  <g style:clip-path={url} class="lc-clip-path-g">
110
136
  {@render children({ id, url, useId })}
111
137
  </g>
138
+ {:else if layerCtx === 'html' && effectivePath}
139
+ <div
140
+ class="lc-clip-path-div"
141
+ style:position="absolute"
142
+ style:inset="0"
143
+ style:clip-path={invert ? `path(evenodd, "${effectivePath}")` : `path("${effectivePath}")`}
144
+ >
145
+ {@render children({ id, url, useId })}
146
+ </div>
147
+ {:else}
148
+ {@render children({ id, url, useId })}
112
149
  {/if}
113
150
  {/if}
@@ -19,8 +19,27 @@ export type ClipPathPropsWithoutHTML = {
19
19
  */
20
20
  disabled?: boolean;
21
21
  /**
22
- * A snippet to insert content into the clipPath.
23
- * Provides the id for the clipPath as a snippet prop.
22
+ * Invert the clip content renders *outside* the shape instead of inside.
23
+ * Implemented by combining the shape with an outer rect covering the chart
24
+ * bounds and applying the even-odd fill rule.
25
+ *
26
+ * @default false
27
+ */
28
+ invert?: boolean;
29
+ /**
30
+ * SVG path `d` string describing the clip shape. When provided, this single
31
+ * value drives all three layers:
32
+ * - SVG: rendered as `<path d={path}>` inside the `<clipPath>`
33
+ * - Canvas: wrapped in `Path2D` and applied via `ctx.clip(...)`
34
+ * - HTML: emitted as CSS `clip-path: path("...")` on a wrapper `<div>`
35
+ *
36
+ * For shapes that can't be expressed as an SVG path (or for advanced
37
+ * per-layer customization), use the `clip` snippet (SVG) alongside `path`.
38
+ */
39
+ path?: string;
40
+ /**
41
+ * A snippet to insert custom SVG content into the `<clipPath>`. When
42
+ * omitted and `path` is set, a `<path d={path}>` is rendered automatically.
24
43
  */
25
44
  clip?: Snippet<[{
26
45
  id: string;
@@ -34,16 +53,6 @@ export type ClipPathPropsWithoutHTML = {
34
53
  url: string;
35
54
  useId?: string;
36
55
  }]>;
37
- /**
38
- * Canvas clip path function. When provided and in canvas mode, sets up a canvas
39
- * clip region by drawing a path and calling `ctx.clip()` before rendering children.
40
- */
41
- canvasClip?: (ctx: CanvasRenderingContext2D) => void;
42
- /**
43
- * Reactive deps for canvas clip invalidation. Return array of values that,
44
- * when changed, should trigger a canvas redraw.
45
- */
46
- canvasClipDeps?: () => any[];
47
56
  };
48
57
  export type ClipPathProps = ClipPathPropsWithoutHTML & Without<SVGAttributes<SVGClipPathElement>, ClipPathPropsWithoutHTML>;
49
58
  declare const ClipPath: import("svelte").Component<ClipPathPropsWithoutHTML, {}, "">;
@@ -46,6 +46,12 @@
46
46
  * @default `d3.curveLinear`
47
47
  */
48
48
  curve?: CurveFactory;
49
+
50
+ /**
51
+ * Interpret `source`/`target` as polar coordinates (`x` = angle, `y` = radius)
52
+ * and render the path in radial space. Defaults to `ctx.radial` when unset.
53
+ */
54
+ radial?: boolean;
49
55
  } & PathPropsWithoutHTML;
50
56
 
51
57
  export type ConnectorProps = ConnectorPropsWithoutHTML &
@@ -57,10 +63,13 @@
57
63
  import {
58
64
  getConnectorD3Path,
59
65
  getConnectorPresetPath,
66
+ getConnectorRadialD3Path,
67
+ getConnectorRadialPresetPath,
60
68
  type ConnectorCoords,
61
69
  type ConnectorSweep,
62
70
  type ConnectorType,
63
71
  } from '../utils/connectorUtils.js';
72
+ import { getChartContext } from '../contexts/chart.js';
64
73
  import Path, { type PathProps, type PathPropsWithoutHTML } from './Path.svelte';
65
74
  import type { Without } from '../utils/types.js';
66
75
  import { createId } from '../utils/createId.js';
@@ -82,6 +91,7 @@
82
91
  type = 'rounded',
83
92
  radius = 20,
84
93
  curve = curveLinear,
94
+ radial: radialProp,
85
95
  pathRef = $bindable(),
86
96
  pathData: pathDataProp,
87
97
  marker,
@@ -92,6 +102,9 @@
92
102
  ...restProps
93
103
  }: ConnectorProps = $props();
94
104
 
105
+ const ctx = getChartContext();
106
+ const radial = $derived(radialProp ?? ctx.radial ?? false);
107
+
95
108
  const sweep = $derived.by(() => {
96
109
  if (sweepProp) return sweepProp;
97
110
  if (type === 'd3') return 'none';
@@ -116,6 +129,11 @@
116
129
 
117
130
  const pathData = $derived.by(() => {
118
131
  if (pathDataProp) return pathDataProp;
132
+ if (radial) {
133
+ return type === 'd3'
134
+ ? getConnectorRadialD3Path({ source, target, curve })
135
+ : getConnectorRadialPresetPath({ source, target, type, radius });
136
+ }
119
137
  if (type === 'd3') {
120
138
  return getConnectorD3Path({
121
139
  source,
@@ -40,6 +40,11 @@ export type ConnectorPropsWithoutHTML = {
40
40
  * @default `d3.curveLinear`
41
41
  */
42
42
  curve?: CurveFactory;
43
+ /**
44
+ * Interpret `source`/`target` as polar coordinates (`x` = angle, `y` = radius)
45
+ * and render the path in radial space. Defaults to `ctx.radial` when unset.
46
+ */
47
+ radial?: boolean;
43
48
  } & PathPropsWithoutHTML;
44
49
  export type ConnectorProps = ConnectorPropsWithoutHTML & Without<PathProps, ConnectorPropsWithoutHTML>;
45
50
  import { type CurveFactory } from 'd3-shape';
@@ -310,6 +310,15 @@
310
310
  const staticStrokeWidth = $derived(typeof strokeWidth === 'number' ? strokeWidth : undefined);
311
311
  const staticOpacity = $derived(typeof opacity === 'number' ? opacity : undefined);
312
312
  const staticClassName = $derived(typeof className === 'string' ? className : undefined);
313
+ // Match SVG's implicit `stroke-width: 1` default: if `stroke` is set but
314
+ // `strokeWidth` is not, render a 1px border so HTML matches SVG/Canvas layers.
315
+ const staticBorderWidth = $derived(
316
+ typeof strokeWidth === 'number'
317
+ ? `${strokeWidth}px`
318
+ : typeof stroke === 'string'
319
+ ? '1px'
320
+ : undefined
321
+ );
313
322
 
314
323
  chartCtx.registerComponent({
315
324
  name: 'Ellipse',
@@ -401,6 +410,12 @@
401
410
  {@const resolvedStrokeWidth = resolveStyleProp(strokeWidth, item.d)}
402
411
  {@const resolvedOpacity = resolveStyleProp(opacity, item.d)}
403
412
  {@const resolvedClass = resolveStyleProp(className, item.d)}
413
+ {@const resolvedBorderWidth =
414
+ resolvedStrokeWidth != null
415
+ ? `${resolvedStrokeWidth}px`
416
+ : resolvedStroke != null
417
+ ? '1px'
418
+ : undefined}
404
419
  <div
405
420
  style:position="absolute"
406
421
  style:left="{item.cx}px"
@@ -408,9 +423,10 @@
408
423
  style:width="{item.rx * 2}px"
409
424
  style:height="{item.ry * 2}px"
410
425
  style:border-radius="50%"
411
- style:background-color={resolvedFill}
426
+ style:background={resolvedFill}
427
+ style:background-origin="border-box"
412
428
  style:opacity={resolvedOpacity}
413
- style:border-width={resolvedStrokeWidth}
429
+ style:border-width={resolvedBorderWidth}
414
430
  style:border-color={resolvedStroke}
415
431
  style:border-style="solid"
416
432
  style:transform="translate(-50%, -50%)"
@@ -426,9 +442,10 @@
426
442
  style:width="{motionRx.current * 2}px"
427
443
  style:height="{motionRy.current * 2}px"
428
444
  style:border-radius="50%"
429
- style:background-color={staticFill}
445
+ style:background={staticFill}
446
+ style:background-origin="border-box"
430
447
  style:opacity={staticOpacity}
431
- style:border-width={staticStrokeWidth}
448
+ style:border-width={staticBorderWidth}
432
449
  style:border-color={staticStroke}
433
450
  style:border-style="solid"
434
451
  style:transform="translate(-50%, -50%)"
@@ -454,8 +471,12 @@
454
471
  }
455
472
 
456
473
  /* Html layers */
457
- :global(:where(.lc-layout-html .lc-ellipse):not([background-color])) {
458
- background-color: var(--fill-color);
474
+ :global(:where(.lc-layout-html .lc-ellipse)) {
475
+ /* Match SVG sizing (visual extent equals `rx * 2`×`ry * 2`, border on outer edge) */
476
+ box-sizing: border-box;
477
+ }
478
+ :global(:where(.lc-layout-html .lc-ellipse):not([background])) {
479
+ background: var(--fill-color);
459
480
  }
460
481
  :global(:where(.lc-layout-html .lc-ellipse):not([border-color])) {
461
482
  border-color: var(--stroke-color);
@@ -24,6 +24,13 @@
24
24
  */
25
25
  disabled?: boolean;
26
26
 
27
+ /**
28
+ * Invert the clip — content renders *outside* the geojson shape.
29
+ *
30
+ * @default false
31
+ */
32
+ invert?: boolean;
33
+
27
34
  /**
28
35
  * The children snippet to render content inside the clipPath.
29
36
  */
@@ -40,9 +47,7 @@
40
47
  import { geoPath as d3GeoPath } from 'd3-geo';
41
48
 
42
49
  import ClipPath from './ClipPath.svelte';
43
- import GeoPath from './GeoPath.svelte';
44
50
  import { createId } from '../utils/createId.js';
45
- import { extractLayerProps } from '../utils/attributes.js';
46
51
  import { getGeoContext } from '../contexts/geo.js';
47
52
 
48
53
  const uid = $props.id();
@@ -51,25 +56,17 @@
51
56
  id = createId('clipPath-', uid),
52
57
  geojson,
53
58
  disabled = false,
59
+ invert = false,
54
60
  children,
55
- ...restProps
56
61
  }: GeoClipPathProps = $props();
57
62
 
58
63
  const geo = getGeoContext();
59
64
 
60
- function canvasClip(ctx: CanvasRenderingContext2D) {
61
- if (!geo.projection || !geojson) return;
62
- const pathGen = d3GeoPath(geo.projection, ctx);
63
- pathGen(geojson);
64
- }
65
-
66
- function canvasClipDeps() {
67
- return [geojson, geo.projection];
68
- }
65
+ // d3-geo-path emits an SVG path `d` string that Path2D and
66
+ // `clip-path: path()` also accept — single source of truth for all layers.
67
+ const path = $derived(
68
+ geo.projection && geojson ? (d3GeoPath(geo.projection)(geojson) ?? undefined) : undefined
69
+ );
69
70
  </script>
70
71
 
71
- <ClipPath {id} {disabled} {children} {canvasClip} {canvasClipDeps}>
72
- {#snippet clip()}
73
- <GeoPath {geojson} class="stroke-none" {...extractLayerProps(restProps, 'lc-clip-path-geo')} />
74
- {/snippet}
75
- </ClipPath>
72
+ <ClipPath {id} {disabled} {invert} {children} {path} />
@@ -17,6 +17,12 @@ export type BaseGeoClipPathPropsWithoutHTML = {
17
17
  * @default false
18
18
  */
19
19
  disabled?: boolean;
20
+ /**
21
+ * Invert the clip — content renders *outside* the geojson shape.
22
+ *
23
+ * @default false
24
+ */
25
+ invert?: boolean;
20
26
  /**
21
27
  * The children snippet to render content inside the clipPath.
22
28
  */
@@ -258,9 +258,7 @@
258
258
  const width = $derived(Math.ceil(barWidth) + padding * 2);
259
259
  const svgHeight = $derived(titleHeight + height + tickLabelHeight + padding * 2 + 3);
260
260
  const barY = $derived(
261
- labelPlacement === 'top'
262
- ? titleHeight + padding + tickLabelHeight
263
- : titleHeight + padding
261
+ labelPlacement === 'top' ? titleHeight + padding + tickLabelHeight : titleHeight + padding
264
262
  );
265
263
  const tickLabelY = $derived(
266
264
  labelPlacement === 'top'