layerchart 2.0.0-next.3 → 2.0.0-next.31

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 (112) hide show
  1. package/dist/components/AnnotationPoint.svelte +16 -9
  2. package/dist/components/AnnotationRange.svelte +3 -3
  3. package/dist/components/Arc.svelte +2 -2
  4. package/dist/components/Axis.svelte +83 -29
  5. package/dist/components/Axis.svelte.d.ts +13 -3
  6. package/dist/components/Bar.svelte +12 -8
  7. package/dist/components/Blur.svelte +5 -3
  8. package/dist/components/Blur.svelte.d.ts +2 -5
  9. package/dist/components/BrushContext.svelte +1 -1
  10. package/dist/components/Calendar.svelte +10 -6
  11. package/dist/components/Calendar.svelte.d.ts +2 -1
  12. package/dist/components/Chart.svelte +39 -3
  13. package/dist/components/Chart.svelte.d.ts +11 -0
  14. package/dist/components/Connector.svelte +2 -2
  15. package/dist/components/Connector.svelte.d.ts +1 -1
  16. package/dist/components/Ellipse.svelte +187 -0
  17. package/dist/components/Ellipse.svelte.d.ts +64 -0
  18. package/dist/components/ForceSimulation.svelte +184 -50
  19. package/dist/components/ForceSimulation.svelte.d.ts +88 -21
  20. package/dist/components/GeoPath.svelte +12 -5
  21. package/dist/components/GeoPoint.svelte +1 -2
  22. package/dist/components/GeoSpline.svelte +4 -4
  23. package/dist/components/GeoSpline.svelte.d.ts +1 -1
  24. package/dist/components/Group.svelte +2 -2
  25. package/dist/components/Highlight.svelte +9 -6
  26. package/dist/components/Hull.svelte +1 -1
  27. package/dist/components/Labels.svelte +3 -2
  28. package/dist/components/Labels.svelte.d.ts +2 -2
  29. package/dist/components/Legend.svelte +19 -12
  30. package/dist/components/Legend.svelte.d.ts +5 -5
  31. package/dist/components/MonthPath.svelte +14 -11
  32. package/dist/components/MonthPath.svelte.d.ts +4 -3
  33. package/dist/components/Polygon.svelte +285 -0
  34. package/dist/components/Polygon.svelte.d.ts +115 -0
  35. package/dist/components/RadialGradient.svelte +1 -3
  36. package/dist/components/Spline.svelte +30 -18
  37. package/dist/components/Spline.svelte.d.ts +12 -4
  38. package/dist/components/Text.svelte +62 -60
  39. package/dist/components/Text.svelte.d.ts +6 -0
  40. package/dist/components/TransformControls.svelte +16 -20
  41. package/dist/components/Treemap.svelte +63 -26
  42. package/dist/components/Treemap.svelte.d.ts +11 -11
  43. package/dist/components/Voronoi.svelte +51 -33
  44. package/dist/components/Voronoi.svelte.d.ts +3 -1
  45. package/dist/components/charts/ArcChart.svelte +5 -3
  46. package/dist/components/charts/AreaChart.svelte +11 -11
  47. package/dist/components/charts/BarChart.svelte +72 -53
  48. package/dist/components/charts/DefaultTooltip.svelte +1 -1
  49. package/dist/components/charts/LineChart.svelte +10 -6
  50. package/dist/components/charts/PieChart.svelte +5 -3
  51. package/dist/components/charts/ScatterChart.svelte +2 -3
  52. package/dist/components/charts/utils.svelte.d.ts +2 -2
  53. package/dist/components/charts/utils.svelte.js +5 -1
  54. package/dist/components/index.d.ts +4 -0
  55. package/dist/components/index.js +5 -1
  56. package/dist/components/layout/Canvas.svelte +67 -49
  57. package/dist/components/layout/Canvas.svelte.d.ts +6 -0
  58. package/dist/components/layout/Layer.svelte +6 -4
  59. package/dist/components/layout/Layer.svelte.d.ts +6 -4
  60. package/dist/components/tooltip/Tooltip.svelte +14 -7
  61. package/dist/components/tooltip/TooltipContext.svelte +136 -43
  62. package/dist/components/tooltip/TooltipContext.svelte.d.ts +3 -3
  63. package/dist/components/tooltip/TooltipHeader.svelte +5 -4
  64. package/dist/components/tooltip/TooltipHeader.svelte.d.ts +3 -3
  65. package/dist/components/tooltip/TooltipItem.svelte +5 -4
  66. package/dist/components/tooltip/TooltipItem.svelte.d.ts +3 -3
  67. package/dist/components/tooltip/TooltipList.svelte +1 -1
  68. package/dist/components/tooltip/tooltipMetaContext.d.ts +2 -2
  69. package/dist/docs/Blockquote.svelte +6 -4
  70. package/dist/docs/Blockquote.svelte.d.ts +4 -19
  71. package/dist/docs/Code.svelte +20 -12
  72. package/dist/docs/Code.svelte.d.ts +9 -23
  73. package/dist/docs/Header1.svelte +4 -2
  74. package/dist/docs/Header1.svelte.d.ts +4 -28
  75. package/dist/docs/Json.svelte +11 -3
  76. package/dist/docs/Json.svelte.d.ts +9 -21
  77. package/dist/docs/Layout.svelte +10 -7
  78. package/dist/docs/Layout.svelte.d.ts +4 -19
  79. package/dist/docs/Link.svelte +7 -3
  80. package/dist/docs/Link.svelte.d.ts +4 -38
  81. package/dist/docs/Preview.svelte +6 -3
  82. package/dist/docs/TilesetField.svelte +20 -19
  83. package/dist/docs/TilesetField.svelte.d.ts +5 -22
  84. package/dist/docs/ViewSourceButton.svelte +9 -6
  85. package/dist/docs/ViewSourceButton.svelte.d.ts +7 -21
  86. package/dist/utils/arcText.svelte.js +4 -4
  87. package/dist/utils/array.d.ts +11 -0
  88. package/dist/utils/array.js +23 -0
  89. package/dist/utils/array.test.d.ts +1 -0
  90. package/dist/utils/array.test.js +200 -0
  91. package/dist/utils/canvas.d.ts +77 -0
  92. package/dist/utils/canvas.js +105 -41
  93. package/dist/utils/genData.d.ts +14 -0
  94. package/dist/utils/genData.js +24 -6
  95. package/dist/utils/index.d.ts +1 -0
  96. package/dist/utils/index.js +1 -0
  97. package/dist/utils/path.d.ts +10 -0
  98. package/dist/utils/path.js +30 -0
  99. package/dist/utils/rect.svelte.d.ts +2 -2
  100. package/dist/utils/rect.svelte.js +69 -1
  101. package/dist/utils/scales.svelte.d.ts +3 -2
  102. package/dist/utils/scales.svelte.js +7 -3
  103. package/dist/utils/shape.d.ts +43 -0
  104. package/dist/utils/shape.js +59 -0
  105. package/dist/utils/string.d.ts +49 -0
  106. package/dist/utils/string.js +4 -2
  107. package/dist/utils/ticks.d.ts +15 -4
  108. package/dist/utils/ticks.js +144 -158
  109. package/dist/utils/ticks.test.js +11 -16
  110. package/dist/utils/treemap.d.ts +1 -1
  111. package/package.json +27 -25
  112. package/dist/utils/object.js +0 -2
@@ -97,6 +97,20 @@
97
97
  */
98
98
  verticalAnchor?: 'start' | 'middle' | 'end' | 'inherit';
99
99
 
100
+ /**
101
+ * The dominant baseline of the text. Useful for aligning text to the baseline of the axis.
102
+ *
103
+ * @default 'auto'
104
+ */
105
+ dominantBaseline?:
106
+ | 'auto'
107
+ | 'text-before-edge'
108
+ | 'text-after-edge'
109
+ | 'middle'
110
+ | 'hanging'
111
+ | 'ideographic'
112
+ | 'mathematical';
113
+
100
114
  /**
101
115
  * Rotational angle of the text
102
116
  */
@@ -178,20 +192,6 @@
178
192
  import { degreesToRadians } from '../utils/math.js';
179
193
  import { createId } from '../utils/createId.js';
180
194
 
181
- /*
182
- TODO:
183
- - [ ] Handle styled text (use <slot /> to measure?)
184
- - [ ] Simplify by using `alignment-baseline` / `dominant-baseline`, rework multiline or drop support, etc
185
- - https://svelte.dev/repl/f12d3003313a43ba8a0be53e5786f1c7?version=3.44.3
186
- - https://observablehq.com/@neocartocnrs/cheat-sheet-on-texts-in-svg
187
-
188
- Reference:
189
- - https://bl.ocks.org/mbostock/7555321
190
- - https://github.com/airbnb/visx/blob/master/packages/visx-text/src/Text.tsx
191
- - https://airbnb.io/visx/text
192
- - https://github.com/airbnb/visx/blob/master/packages/visx-demo/src/pages/text.tsx
193
- */
194
-
195
195
  const uid = $props.id();
196
196
 
197
197
  let {
@@ -208,6 +208,7 @@
208
208
  scaleToFit = false,
209
209
  textAnchor = 'start',
210
210
  verticalAnchor = 'end',
211
+ dominantBaseline = 'auto',
211
212
  rotate,
212
213
  opacity = 1,
213
214
  strokeWidth = 0,
@@ -227,6 +228,8 @@
227
228
  ...restProps
228
229
  }: TextProps = $props();
229
230
 
231
+ const renderCtx = getRenderContext();
232
+
230
233
  let ref = $state<SVGTextElement>();
231
234
  let svgRef = $state<SVGElement>();
232
235
  let pathRef = $state<SVGPathElement>();
@@ -260,49 +263,56 @@
260
263
  };
261
264
  });
262
265
 
263
- const rawText = $derived(value != null ? value.toString() : '');
266
+ // Handle null and convert `\n` strings back to newline characters
267
+ const rawText = $derived(value != null ? value.toString().replace(/\\n/g, '\n') : '');
264
268
 
265
269
  const textValue = $derived.by(() => {
266
270
  if (!truncateConfig) return rawText;
267
271
  return truncateText(rawText, truncateConfig);
268
272
  });
269
273
 
270
- const renderCtx = getRenderContext();
274
+ const spaceWidth = $derived(getStringWidth('\u00A0', style) || 0);
271
275
 
272
- const words = $derived(textValue ? textValue.split(/(?:(?!\u00A0+)\s+)/) : []);
276
+ const wordsByLines = $derived.by(() => {
277
+ // Split by newlines to preserve explicit line breaks
278
+ const lines = textValue.split('\n');
273
279
 
274
- const wordsWithWidth = $derived(
275
- words.map((word) => ({
276
- word,
277
- width: getStringWidth(word, style) || 0,
278
- }))
279
- );
280
+ return lines.flatMap((line) => {
281
+ // Split each line into words
282
+ const words = line.split(/(?:(?!\u00A0+)\s+)/);
280
283
 
281
- const spaceWidth = $derived(getStringWidth('\u00A0', style) || 0);
282
-
283
- const wordsByLines = $derived(
284
- wordsWithWidth.reduce((result: { words: string[]; width?: number }[], item) => {
285
- const currentLine = result[result.length - 1];
286
-
287
- if (
288
- currentLine &&
289
- (width == null || scaleToFit || (currentLine.width || 0) + item.width + spaceWidth < width)
290
- ) {
291
- // Word can be added to an existing line
292
- currentLine.words.push(item.word);
293
- currentLine.width = currentLine.width || 0;
294
- currentLine.width += item.width + spaceWidth;
284
+ if (width == null) {
285
+ // No width specified, only use explicit line breaks (if used)
286
+ return [{ words }];
295
287
  } else {
296
- // Add first word to line or word is too long to scaleToFit on existing line
297
- const newLine = { words: [item.word], width: item.width };
298
- result.push(newLine);
288
+ // Handle word wrapping within each line
289
+ return words.reduce((result: { words: string[]; width?: number }[], item) => {
290
+ const currentLine = result[result.length - 1];
291
+ const itemWidth = getStringWidth(item, style) || 0;
292
+
293
+ if (
294
+ currentLine &&
295
+ (width == null ||
296
+ scaleToFit ||
297
+ (currentLine.width || 0) + itemWidth + spaceWidth < width)
298
+ ) {
299
+ // Word can be added to an existing line
300
+ currentLine.words.push(item);
301
+ currentLine.width = currentLine.width || 0;
302
+ currentLine.width += itemWidth + spaceWidth;
303
+ } else {
304
+ // Add first word to line or word is too long to scaleToFit on existing line
305
+ const newLine = { words: [item], width: itemWidth };
306
+ result.push(newLine);
307
+ }
308
+
309
+ return result;
310
+ }, []);
299
311
  }
312
+ });
313
+ });
300
314
 
301
- return result;
302
- }, [])
303
- );
304
-
305
- const lines = $derived(wordsByLines.length);
315
+ const lineCount = $derived(wordsByLines.length);
306
316
 
307
317
  /**
308
318
  * Convert css value to pixel value (ex. 0.71em => 11.36)
@@ -327,28 +337,18 @@
327
337
 
328
338
  const startDy = $derived.by(() => {
329
339
  if (verticalAnchor === 'start') {
330
- return getPixelValue(capHeight);
331
- } else if (verticalAnchor === 'middle') {
332
- return ((lines - 1) / 2) * -getPixelValue(lineHeight) + getPixelValue(capHeight) / 2;
333
- } else {
334
- return (lines - 1) * -getPixelValue(lineHeight);
335
- }
336
- });
337
-
338
- const pathStartDy = $derived.by(() => {
339
- if (verticalAnchor === 'start') {
340
- return getPixelValue(capHeight);
340
+ return getPixelValue(lineHeight);
341
341
  } else if (verticalAnchor === 'middle') {
342
- return (0 / 2) * -getPixelValue(lineHeight) + getPixelValue(capHeight) / 2;
342
+ return ((lineCount - 1) / 2) * -getPixelValue(lineHeight) + getPixelValue(capHeight) / 2;
343
343
  } else {
344
- return 0 * -getPixelValue(lineHeight);
344
+ return (lineCount - 1) * -getPixelValue(lineHeight) - getPixelValue(capHeight) / 2;
345
345
  }
346
346
  });
347
347
 
348
348
  const scaleTransform = $derived.by(() => {
349
349
  if (
350
350
  scaleToFit &&
351
- lines > 0 &&
351
+ lineCount > 0 &&
352
352
  typeof x == 'number' &&
353
353
  typeof y == 'number' &&
354
354
  typeof width == 'number'
@@ -500,6 +500,7 @@
500
500
  >
501
501
  <textPath
502
502
  style="text-anchor: {textAnchor};"
503
+ dominant-baseline={dominantBaseline}
503
504
  href="#{pathId}"
504
505
  {startOffset}
505
506
  class={cls(layerClass('text-path'))}
@@ -514,6 +515,7 @@
514
515
  y={motionY.current}
515
516
  {transform}
516
517
  text-anchor={textAnchor}
518
+ dominant-baseline={dominantBaseline}
517
519
  {...restProps}
518
520
  {fill}
519
521
  fill-opacity={fillOpacity}
@@ -525,7 +527,7 @@
525
527
  {#each wordsByLines as line, index}
526
528
  <tspan
527
529
  x={motionX.current}
528
- dy={index === 0 ? startDy : lineHeight}
530
+ dy={index === 0 ? startDy : getPixelValue(lineHeight)}
529
531
  class={layerClass('text-tspan')}
530
532
  >
531
533
  {line.words.join(' ')}
@@ -80,6 +80,12 @@ export type TextPropsWithoutHTML = {
80
80
  * @default 'end'
81
81
  */
82
82
  verticalAnchor?: 'start' | 'middle' | 'end' | 'inherit';
83
+ /**
84
+ * The dominant baseline of the text. Useful for aligning text to the baseline of the axis.
85
+ *
86
+ * @default 'auto'
87
+ */
88
+ dominantBaseline?: 'auto' | 'text-before-edge' | 'text-after-edge' | 'middle' | 'hanging' | 'ideographic' | 'mathematical';
83
89
  /**
84
90
  * Rotational angle of the text
85
91
  */
@@ -34,18 +34,14 @@
34
34
  import { Button, Icon, MenuButton, Tooltip } from 'svelte-ux';
35
35
  import { cls } from '@layerstack/tailwind';
36
36
 
37
- // TODO: maybe we include the icons as I think importing them like this
38
- // will bog down the dev server
39
- import {
40
- mdiArrowULeftTop,
41
- mdiMagnifyPlusOutline,
42
- mdiMagnifyMinusOutline,
43
- mdiImageFilterCenterFocus,
44
- mdiChevronDown,
45
- mdiResize,
46
- mdiArrowExpandAll,
47
- mdiCancel,
48
- } from '@mdi/js';
37
+ import LucideFocus from '~icons/lucide/focus';
38
+ import LucideChevronDown from '~icons/lucide/chevron-down';
39
+ import LucideCircleOff from '~icons/lucide/circle-off';
40
+ import LucideImageUpscale from '~icons/lucide/image-upscale';
41
+ import LucideMove from '~icons/lucide/move';
42
+ import LucideUndo2 from '~icons/lucide/undo-2';
43
+ import LucideZoomIn from '~icons/lucide/zoom-in';
44
+ import LucideZoomOut from '~icons/lucide/zoom-out';
49
45
 
50
46
  import { getTransformContext } from './TransformContext.svelte';
51
47
  import type { Without } from '../utils/types.js';
@@ -113,7 +109,7 @@
113
109
  {#if show.includes('zoomIn')}
114
110
  <Tooltip title="Zoom in">
115
111
  <Button
116
- icon={mdiMagnifyPlusOutline}
112
+ icon={LucideZoomIn}
117
113
  on:click={() => transform.zoomIn()}
118
114
  {size}
119
115
  class="text-surface-content p-2"
@@ -124,7 +120,7 @@
124
120
  {#if show.includes('zoomOut')}
125
121
  <Tooltip title="Zoom out">
126
122
  <Button
127
- icon={mdiMagnifyMinusOutline}
123
+ icon={LucideZoomOut}
128
124
  on:click={() => transform.zoomOut()}
129
125
  {size}
130
126
  class="text-surface-content p-2"
@@ -135,7 +131,7 @@
135
131
  {#if show.includes('center')}
136
132
  <Tooltip title="Center">
137
133
  <Button
138
- icon={mdiImageFilterCenterFocus}
134
+ icon={LucideFocus}
139
135
  on:click={() => transform.translateCenter()}
140
136
  {size}
141
137
  class="text-surface-content p-2"
@@ -146,7 +142,7 @@
146
142
  {#if show.includes('reset')}
147
143
  <Tooltip title="Reset">
148
144
  <Button
149
- icon={mdiArrowULeftTop}
145
+ icon={LucideUndo2}
150
146
  on:click={() => transform.reset()}
151
147
  {size}
152
148
  class="text-surface-content p-2"
@@ -159,9 +155,9 @@
159
155
  <MenuButton
160
156
  iconOnly
161
157
  options={[
162
- { label: 'None', value: 'none', icon: mdiCancel },
163
- { label: 'Zoom', value: 'scale', icon: mdiResize },
164
- { label: 'Move', value: 'translate', icon: mdiArrowExpandAll },
158
+ { label: 'None', value: 'none', icon: LucideCircleOff },
159
+ { label: 'Zoom', value: 'scale', icon: LucideImageUpscale },
160
+ { label: 'Move', value: 'translate', icon: LucideMove },
165
161
  ]}
166
162
  menuProps={{ placement: menuPlacementByOrientationAndPlacement[orientation][placement] }}
167
163
  menuIcon={null}
@@ -171,7 +167,7 @@
171
167
  class="text-surface-content"
172
168
  >
173
169
  <svelte:fragment slot="selection" let:value>
174
- <Icon data={value?.icon ?? mdiChevronDown} />
170
+ <Icon data={value?.icon ?? LucideChevronDown} />
175
171
  </svelte:fragment>
176
172
  </MenuButton>
177
173
  </Tooltip>
@@ -18,53 +18,53 @@
18
18
  *
19
19
  * @default 0
20
20
  */
21
- padding?: number;
21
+ padding?: number | ((node: HierarchyRectangularNode<T>) => number);
22
22
 
23
23
  /**
24
24
  * The inner padding between nodes.
25
25
  *
26
26
  * @default 0
27
27
  */
28
- paddingInner?: number;
28
+ paddingInner?: number | ((node: HierarchyRectangularNode<T>) => number);
29
29
 
30
30
  /**
31
31
  * The outer padding between nodes.
32
32
  *
33
33
  * @default 0
34
34
  */
35
- paddingOuter?: number;
35
+ paddingOuter?: number | ((node: HierarchyRectangularNode<T>) => number);
36
36
 
37
37
  /**
38
38
  * The top padding between nodes.
39
39
  *
40
40
  * @default 0
41
41
  */
42
- paddingTop?: number;
42
+ paddingTop?: number | ((node: HierarchyRectangularNode<T>) => number);
43
43
 
44
44
  /**
45
45
  * The bottom padding between nodes.
46
46
  *
47
47
  * @default 0
48
48
  */
49
- paddingBottom?: number;
49
+ paddingBottom?: number | ((node: HierarchyRectangularNode<T>) => number);
50
50
  /**
51
51
  * The left padding between nodes.
52
52
  *
53
53
  */
54
- paddingLeft?: number;
54
+ paddingLeft?: number | ((node: HierarchyRectangularNode<T>) => number);
55
55
 
56
56
  /**
57
57
  * The right padding between nodes.
58
58
  *
59
59
  */
60
- paddingRight?: number;
60
+ paddingRight?: number | ((node: HierarchyRectangularNode<T>) => number);
61
61
 
62
62
  /**
63
- * The selected node.
63
+ * Modify tiling function for approapriate aspect ratio when treemap is zoomed in
64
64
  *
65
- * @default null
65
+ * @default false
66
66
  */
67
- selected?: HierarchyRectangularNode<T> | null;
67
+ maintainAspectRatio?: boolean;
68
68
 
69
69
  hierarchy?: HierarchyNode<T>;
70
70
 
@@ -99,7 +99,7 @@
99
99
  paddingBottom = 0,
100
100
  paddingLeft,
101
101
  paddingRight,
102
- selected = $bindable(null),
102
+ maintainAspectRatio = false,
103
103
  children,
104
104
  }: TreemapProps<T> = $props();
105
105
 
@@ -121,45 +121,82 @@
121
121
  : tile
122
122
  );
123
123
 
124
- const treemap = $derived.by(() => {
124
+ const treemapData = $derived.by(() => {
125
125
  const _treemap = d3treemap<T>()
126
126
  .size([ctx.width, ctx.height])
127
- .tile(aspectTile(tileFunc, ctx.width, ctx.height));
127
+ .tile(maintainAspectRatio ? aspectTile(tileFunc, ctx.width, ctx.height) : tileFunc);
128
128
 
129
129
  if (padding) {
130
- _treemap.padding(padding);
130
+ // Make Typescript happy to pick the correct overload
131
+ // TODO: Better way to do this?
132
+ if (typeof padding === 'number') {
133
+ _treemap.padding(padding);
134
+ } else {
135
+ _treemap.padding(padding);
136
+ }
131
137
  }
132
138
 
133
139
  if (paddingInner) {
134
- _treemap.paddingInner(paddingInner);
140
+ if (typeof paddingInner === 'number') {
141
+ _treemap.paddingInner(typeof paddingInner === 'number' ? paddingInner : paddingInner);
142
+ } else {
143
+ _treemap.paddingInner(paddingInner);
144
+ }
135
145
  }
136
146
 
137
147
  if (paddingOuter) {
138
- _treemap.paddingOuter(paddingOuter);
148
+ if (typeof paddingOuter === 'number') {
149
+ _treemap.paddingOuter(paddingOuter);
150
+ } else {
151
+ _treemap.paddingOuter(paddingOuter);
152
+ }
139
153
  }
140
154
 
141
155
  if (paddingTop) {
142
- _treemap.paddingTop(paddingTop);
156
+ if (typeof paddingTop === 'number') {
157
+ _treemap.paddingTop(paddingTop);
158
+ } else {
159
+ _treemap.paddingTop(paddingTop);
160
+ }
143
161
  }
144
162
 
145
163
  if (paddingBottom) {
146
- _treemap.paddingBottom(paddingBottom);
164
+ if (typeof paddingBottom === 'number') {
165
+ _treemap.paddingBottom(paddingBottom);
166
+ } else {
167
+ _treemap.paddingBottom(paddingBottom);
168
+ }
147
169
  }
148
170
 
149
171
  if (paddingLeft) {
150
- _treemap.paddingLeft(paddingLeft);
172
+ if (typeof paddingLeft === 'number') {
173
+ _treemap.paddingLeft(paddingLeft);
174
+ } else {
175
+ _treemap.paddingLeft(paddingLeft);
176
+ }
151
177
  }
152
178
  if (paddingRight) {
153
- _treemap.paddingRight(paddingRight);
179
+ if (typeof paddingRight === 'number') {
180
+ _treemap.paddingRight(paddingRight);
181
+ } else {
182
+ _treemap.paddingRight(paddingRight);
183
+ }
154
184
  }
155
- return _treemap;
156
- });
157
185
 
158
- const treemapData = $derived(hierarchy ? treemap(hierarchy) : null);
186
+ if (hierarchy) {
187
+ const h = hierarchy.copy();
188
+ const treemapData = _treemap(h);
189
+ return {
190
+ links: treemapData.links(),
191
+ nodes: treemapData.descendants(),
192
+ };
193
+ }
159
194
 
160
- $effect.pre(() => {
161
- selected = treemapData;
195
+ return {
196
+ links: [],
197
+ nodes: [],
198
+ };
162
199
  });
163
200
  </script>
164
201
 
165
- {@render children?.({ nodes: treemapData ? treemapData.descendants() : [] })}
202
+ {@render children?.({ nodes: treemapData.nodes })}
@@ -10,47 +10,47 @@ export type TreemapProps<T> = {
10
10
  *
11
11
  * @default 0
12
12
  */
13
- padding?: number;
13
+ padding?: number | ((node: HierarchyRectangularNode<T>) => number);
14
14
  /**
15
15
  * The inner padding between nodes.
16
16
  *
17
17
  * @default 0
18
18
  */
19
- paddingInner?: number;
19
+ paddingInner?: number | ((node: HierarchyRectangularNode<T>) => number);
20
20
  /**
21
21
  * The outer padding between nodes.
22
22
  *
23
23
  * @default 0
24
24
  */
25
- paddingOuter?: number;
25
+ paddingOuter?: number | ((node: HierarchyRectangularNode<T>) => number);
26
26
  /**
27
27
  * The top padding between nodes.
28
28
  *
29
29
  * @default 0
30
30
  */
31
- paddingTop?: number;
31
+ paddingTop?: number | ((node: HierarchyRectangularNode<T>) => number);
32
32
  /**
33
33
  * The bottom padding between nodes.
34
34
  *
35
35
  * @default 0
36
36
  */
37
- paddingBottom?: number;
37
+ paddingBottom?: number | ((node: HierarchyRectangularNode<T>) => number);
38
38
  /**
39
39
  * The left padding between nodes.
40
40
  *
41
41
  */
42
- paddingLeft?: number;
42
+ paddingLeft?: number | ((node: HierarchyRectangularNode<T>) => number);
43
43
  /**
44
44
  * The right padding between nodes.
45
45
  *
46
46
  */
47
- paddingRight?: number;
47
+ paddingRight?: number | ((node: HierarchyRectangularNode<T>) => number);
48
48
  /**
49
- * The selected node.
49
+ * Modify tiling function for approapriate aspect ratio when treemap is zoomed in
50
50
  *
51
- * @default null
51
+ * @default false
52
52
  */
53
- selected?: HierarchyRectangularNode<T> | null;
53
+ maintainAspectRatio?: boolean;
54
54
  hierarchy?: HierarchyNode<T>;
55
55
  children?: Snippet<[{
56
56
  nodes: HierarchyRectangularNode<T>[];
@@ -62,7 +62,7 @@ declare class __sveltets_Render<T> {
62
62
  props(): TreemapProps<T>;
63
63
  events(): {};
64
64
  slots(): {};
65
- bindings(): "selected";
65
+ bindings(): "";
66
66
  exports(): {};
67
67
  }
68
68
  interface $$IsomorphicComponent {
@@ -1,6 +1,5 @@
1
1
  <script lang="ts" module>
2
2
  import type { Without } from '../utils/types.js';
3
- import type { SVGAttributes } from 'svelte/elements';
4
3
 
5
4
  export type VoronoiPropsWithoutHTML = {
6
5
  /**
@@ -8,6 +7,9 @@
8
7
  */
9
8
  data?: any;
10
9
 
10
+ /** Radius to clip voronoi cells. `0` or `undefined` to disables clipping */
11
+ r?: number;
12
+
11
13
  /**
12
14
  * Classes to apply to the root and path elements
13
15
  *
@@ -56,7 +58,7 @@
56
58
  <script lang="ts">
57
59
  import { min } from 'd3-array';
58
60
  import { Delaunay } from 'd3-delaunay';
59
- import type { GeoPermissibleObjects } from 'd3-geo';
61
+ import { type GeoPermissibleObjects } from 'd3-geo';
60
62
  // @ts-expect-error
61
63
  import { geoVoronoi } from 'd3-geo-voronoi';
62
64
  import { pointRadial } from 'd3-shape';
@@ -67,10 +69,13 @@
67
69
  import Spline from './Spline.svelte';
68
70
  import { getChartContext } from './Chart.svelte';
69
71
  import { getGeoContext } from './GeoContext.svelte';
72
+ import CircleClipPath from './CircleClipPath.svelte';
73
+
70
74
  import { layerClass } from '../utils/attributes.js';
71
75
 
72
76
  let {
73
77
  data,
78
+ r,
74
79
  classes = {},
75
80
  onclick,
76
81
  onpointerenter,
@@ -109,53 +114,66 @@
109
114
  // Width and/or height can sometimes be negative (when loading data remotely and updately)
110
115
  const boundWidth = $derived(Math.max(ctx.width, 0));
111
116
  const boundHeight = $derived(Math.max(ctx.height, 0));
117
+
118
+ const disableClip = $derived(r === 0 || r == null || r === Infinity);
112
119
  </script>
113
120
 
114
121
  <Group {...restProps} class={cls(layerClass('voronoi-g'), classes.root, className)}>
115
122
  {#if geo.projection}
116
123
  {@const polygons = geoVoronoi().polygons(points)}
117
124
  {#each polygons.features as feature}
118
- <GeoPath
119
- geojson={feature}
120
- class={cls(
121
- layerClass('voronoi-geo-path'),
122
- 'fill-transparent stroke-transparent',
123
- classes.path
124
- )}
125
- onclick={(e) => onclick?.(e, { data: feature.properties.site.data, feature })}
126
- onpointerenter={(e) => onpointerenter?.(e, { data: feature.properties.site.data, feature })}
127
- onpointermove={(e) => onpointermove?.(e, { data: feature.properties.site.data, feature })}
128
- onpointerdown={(e) => onpointerdown?.(e, { data: feature.properties.site.data, feature })}
129
- {onpointerleave}
130
- ontouchmove={(e) => {
131
- // Prevent touch to not interfere with pointer
132
- e.preventDefault();
133
- }}
134
- />
135
- {/each}
136
- {:else}
137
- {@const voronoi = Delaunay.from(points).voronoi([0, 0, boundWidth, boundHeight])}
138
- {#each points as point, i}
139
- {@const pathData = voronoi.renderCell(i)}
140
- <!-- Wait to render Spline until pathData is available to fix path artifacts from injected tweened points in Spline -->
141
- {#if pathData}
142
- <Spline
143
- {pathData}
125
+ {@const point = r ? geo.projection?.(feature.properties.sitecoordinates) : null}
126
+ <CircleClipPath
127
+ cx={point?.[0]}
128
+ cy={point?.[1]}
129
+ r={r ?? 0}
130
+ disabled={point == null || disableClip}
131
+ >
132
+ <GeoPath
133
+ geojson={feature}
144
134
  class={cls(
145
- layerClass('voronoi-path'),
135
+ layerClass('voronoi-geo-path'),
146
136
  'fill-transparent stroke-transparent',
147
137
  classes.path
148
138
  )}
149
- onclick={(e) => onclick?.(e, { data: point.data, point })}
150
- onpointerenter={(e) => onpointerenter?.(e, { data: point.data, point })}
151
- onpointermove={(e) => onpointermove?.(e, { data: point.data, point })}
139
+ onclick={(e) => onclick?.(e, { data: feature.properties.site.data, feature })}
140
+ onpointerenter={(e) =>
141
+ onpointerenter?.(e, { data: feature.properties.site.data, feature })}
142
+ onpointermove={(e) => onpointermove?.(e, { data: feature.properties.site.data, feature })}
143
+ onpointerdown={(e) => onpointerdown?.(e, { data: feature.properties.site.data, feature })}
152
144
  {onpointerleave}
153
- onpointerdown={(e) => onpointerdown?.(e, { data: point.data, point })}
154
145
  ontouchmove={(e) => {
155
146
  // Prevent touch to not interfere with pointer
156
147
  e.preventDefault();
157
148
  }}
158
149
  />
150
+ </CircleClipPath>
151
+ {/each}
152
+ {:else}
153
+ {@const voronoi = Delaunay.from(points).voronoi([0, 0, boundWidth, boundHeight])}
154
+ {#each points as point, i}
155
+ {@const pathData = voronoi.renderCell(i)}
156
+ <!-- Wait to render Spline until pathData is available to fix path artifacts from injected tweened points in Spline -->
157
+ {#if pathData}
158
+ <CircleClipPath cx={point[0]} cy={point[1]} r={r ?? 0} disabled={disableClip}>
159
+ <Spline
160
+ {pathData}
161
+ class={cls(
162
+ layerClass('voronoi-path'),
163
+ 'fill-transparent stroke-transparent',
164
+ classes.path
165
+ )}
166
+ onclick={(e) => onclick?.(e, { data: point.data, point })}
167
+ onpointerenter={(e) => onpointerenter?.(e, { data: point.data, point })}
168
+ onpointermove={(e) => onpointermove?.(e, { data: point.data, point })}
169
+ {onpointerleave}
170
+ onpointerdown={(e) => onpointerdown?.(e, { data: point.data, point })}
171
+ ontouchmove={(e) => {
172
+ // Prevent touch to not interfere with pointer
173
+ e.preventDefault();
174
+ }}
175
+ />
176
+ </CircleClipPath>
159
177
  {/if}
160
178
  {/each}
161
179
  {/if}