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
@@ -64,6 +64,13 @@
64
64
  */
65
65
  ignoreTransform?: boolean;
66
66
 
67
+ /**
68
+ * Disable the hit canvas (useful when animations are playing)
69
+ *
70
+ * @default false
71
+ */
72
+ disableHitCanvas?: boolean;
73
+
67
74
  /**
68
75
  * Show the hit canvas for debugging purposes.
69
76
  *
@@ -144,13 +151,13 @@
144
151
  import { onMount, untrack, type Snippet } from 'svelte';
145
152
  import { cls } from '@layerstack/tailwind';
146
153
  import { Logger, localPoint } from '@layerstack/utils';
147
- import { darkColorScheme } from '@layerstack/svelte-stores';
154
+ import { MediaQueryPresets } from '@layerstack/svelte-state';
148
155
 
149
156
  import { setRenderContext } from '../Chart.svelte';
150
157
  import { getTransformContext } from '../TransformContext.svelte';
151
158
  import { getPixelColor, scaleCanvas, type ComputedStylesOptions } from '../../utils/canvas.js';
152
159
  import { getColorStr, rgbColorGenerator } from '../../utils/color.js';
153
- import { Context } from 'runed';
160
+ import { Context, useMutationObserver, watch } from 'runed';
154
161
  import type {
155
162
  HTMLCanvasAttributes,
156
163
  MouseEventHandler,
@@ -171,6 +178,7 @@
171
178
  fallback,
172
179
  center = false,
173
180
  ignoreTransform = false,
181
+ disableHitCanvas = false,
174
182
  class: className,
175
183
  children,
176
184
  onclick,
@@ -256,6 +264,23 @@
256
264
  * end HitCanvas
257
265
  */
258
266
 
267
+ // Invalidate/redraw if color scheme changes, either via browser `prefers-color-scheme` (including emulation) or by changing `<html class="dark">` or `<html data-theme="...">`
268
+ const { dark } = new MediaQueryPresets();
269
+ watch(
270
+ () => dark.current,
271
+ () => {
272
+ canvasContext.invalidate();
273
+ }
274
+ );
275
+ useMutationObserver(
276
+ () => document.documentElement,
277
+ () => canvasContext.invalidate(),
278
+ {
279
+ attributes: true,
280
+ attributeFilter: ['class', 'data-theme'],
281
+ }
282
+ );
283
+
259
284
  onMount(() => {
260
285
  context = ref?.getContext('2d', { willReadFrequently }) as CanvasRenderingContext2D;
261
286
 
@@ -263,22 +288,7 @@
263
288
  willReadFrequently: false, // Explicitly set to `false` to resolve pixel artifacts between fill and stroke with the same color (issue #372)
264
289
  }) as CanvasRenderingContext2D;
265
290
 
266
- // Invalidate/redraw if color scheme changes, either via browser `prefers-color-scheme` (including emulation) or by changing `<html class="dark">` or `<html data-theme="...">`
267
- darkColorScheme.subscribe(() => {
268
- canvasContext.invalidate();
269
- });
270
-
271
- const observer = new MutationObserver(() => {
272
- canvasContext.invalidate();
273
- });
274
-
275
- observer.observe(document.documentElement, {
276
- attributes: true,
277
- attributeFilter: ['class', 'data-theme'],
278
- });
279
-
280
291
  return () => {
281
- observer.disconnect();
282
292
  if (frameId) {
283
293
  cancelAnimationFrame(frameId);
284
294
  }
@@ -336,45 +346,53 @@
336
346
  context.restore();
337
347
  }
338
348
 
339
- // sync hit canvas with main canvas
349
+ /*
350
+ * Sync hit canvas with main canvas
351
+ */
340
352
  if (hitCanvasContext) {
341
- // scale hit canvas to match main canvas
342
- scaleCanvas(hitCanvasContext, ctx.containerWidth, ctx.containerHeight);
343
- hitCanvasContext.clearRect(0, 0, ctx.containerWidth, ctx.containerHeight);
344
-
345
- // reset and sync transform to the state after retainState components
346
- hitCanvasContext.resetTransform();
347
- hitCanvasContext.setTransform(mainTransformAfterRetain);
348
-
349
- // reset color generator
350
- colorGenerator = rgbColorGenerator();
351
-
352
353
  const inactiveMoving = !activeCanvas && transformCtx.moving;
353
-
354
- // render retainState components on hit canvas (e.g., Group)
355
- for (const c of retainStateComponents) {
356
- const componentHasEvents = c.events && Object.values(c.events).filter((d) => d).length > 0;
357
-
358
- if (componentHasEvents && !inactiveMoving && !transformCtx.dragging) {
359
- // since the transform was already applied via setTransform, skip rendering
360
- // the retainState component's transform again; proceed to its children
361
- continue;
354
+ if (disableHitCanvas || transformCtx.dragging || inactiveMoving) {
355
+ // Skip rendering hit canvas
356
+ hitCanvasContext.clearRect(0, 0, ctx.containerWidth, ctx.containerHeight);
357
+ } else {
358
+ // scale hit canvas to match main canvas
359
+ scaleCanvas(hitCanvasContext, ctx.containerWidth, ctx.containerHeight);
360
+ hitCanvasContext.clearRect(0, 0, ctx.containerWidth, ctx.containerHeight);
361
+
362
+ // reset and sync transform to the state after retainState components
363
+ hitCanvasContext.resetTransform();
364
+ hitCanvasContext.setTransform(mainTransformAfterRetain);
365
+
366
+ // reset color generator
367
+ colorGenerator = rgbColorGenerator();
368
+
369
+ // render retainState components on hit canvas (e.g., Group)
370
+ for (const c of retainStateComponents) {
371
+ const componentHasEvents =
372
+ c.events && Object.values(c.events).filter((d) => d).length > 0;
373
+
374
+ if (componentHasEvents) {
375
+ // since the transform was already applied via setTransform, skip rendering
376
+ // the retainState component's transform again; proceed to its children
377
+ continue;
378
+ }
362
379
  }
363
- }
364
380
 
365
- // render non-retainState components on hit canvas
366
- for (const c of nonRetainStateComponents) {
367
- const componentHasEvents = c.events && Object.values(c.events).filter((d) => d).length > 0;
381
+ // render non-retainState components on hit canvas
382
+ for (const c of nonRetainStateComponents) {
383
+ const componentHasEvents =
384
+ c.events && Object.values(c.events).filter((d) => d).length > 0;
368
385
 
369
- if (componentHasEvents && !inactiveMoving && !transformCtx.dragging) {
370
- const color = getColorStr(colorGenerator.next().value);
371
- const styleOverrides = { styles: { fill: color, stroke: color, _fillOpacity: 0.1 } };
386
+ if (componentHasEvents) {
387
+ const color = getColorStr(colorGenerator.next().value);
388
+ const styleOverrides = { styles: { fill: color, stroke: color, _fillOpacity: 0.1 } };
372
389
 
373
- hitCanvasContext.save();
374
- c.render(hitCanvasContext, styleOverrides);
375
- hitCanvasContext.restore();
390
+ hitCanvasContext.save();
391
+ c.render(hitCanvasContext, styleOverrides);
392
+ hitCanvasContext.restore();
376
393
 
377
- componentByColor.set(color, c);
394
+ componentByColor.set(color, c);
395
+ }
378
396
  }
379
397
  }
380
398
  }
@@ -55,6 +55,12 @@ export type CanvasPropsWithoutHTML = {
55
55
  * @default false
56
56
  */
57
57
  ignoreTransform?: boolean;
58
+ /**
59
+ * Disable the hit canvas (useful when animations are playing)
60
+ *
61
+ * @default false
62
+ */
63
+ disableHitCanvas?: boolean;
58
64
  /**
59
65
  * Show the hit canvas for debugging purposes.
60
66
  *
@@ -3,17 +3,19 @@
3
3
 
4
4
  export type CanvasLayerProps = {
5
5
  type: 'canvas';
6
- } & Omit<ComponentProps<typeof Canvas>, 'type'>;
6
+ } & Omit<ComponentProps<typeof Canvas>, 'type' | 'onpointermove'>;
7
7
 
8
8
  export type HtmlLayerProps = {
9
9
  type: 'html';
10
- } & Omit<ComponentProps<typeof Html>, 'type'>;
10
+ } & Omit<ComponentProps<typeof Html>, 'type' | 'onpointermove'>;
11
11
 
12
12
  export type SvgLayerProps = {
13
13
  type: 'svg';
14
- } & Omit<ComponentProps<typeof Svg>, 'type'>;
14
+ } & Omit<ComponentProps<typeof Svg>, 'type' | 'onpointermove'>;
15
15
 
16
- export type LayerProps = CanvasLayerProps | HtmlLayerProps | SvgLayerProps;
16
+ export type LayerProps = (CanvasLayerProps | HtmlLayerProps | SvgLayerProps) & {
17
+ onpointermove?: (e: PointerEvent) => void;
18
+ };
17
19
  </script>
18
20
 
19
21
  <script lang="ts">
@@ -1,14 +1,16 @@
1
1
  import type { ComponentProps } from 'svelte';
2
2
  export type CanvasLayerProps = {
3
3
  type: 'canvas';
4
- } & Omit<ComponentProps<typeof Canvas>, 'type'>;
4
+ } & Omit<ComponentProps<typeof Canvas>, 'type' | 'onpointermove'>;
5
5
  export type HtmlLayerProps = {
6
6
  type: 'html';
7
- } & Omit<ComponentProps<typeof Html>, 'type'>;
7
+ } & Omit<ComponentProps<typeof Html>, 'type' | 'onpointermove'>;
8
8
  export type SvgLayerProps = {
9
9
  type: 'svg';
10
- } & Omit<ComponentProps<typeof Svg>, 'type'>;
11
- export type LayerProps = CanvasLayerProps | HtmlLayerProps | SvgLayerProps;
10
+ } & Omit<ComponentProps<typeof Svg>, 'type' | 'onpointermove'>;
11
+ export type LayerProps = (CanvasLayerProps | HtmlLayerProps | SvgLayerProps) & {
12
+ onpointermove?: (e: PointerEvent) => void;
13
+ };
12
14
  import Canvas from './Canvas.svelte';
13
15
  import Html from './Html.svelte';
14
16
  import Svg from './Svg.svelte';
@@ -359,13 +359,8 @@
359
359
  {#if tooltipCtx.data}
360
360
  <div
361
361
  {...props.root}
362
- class={cls(
363
- layerClass('tooltip-root'),
364
- 'absolute z-50 select-none',
365
- !pointerEvents && 'pointer-events-none',
366
- classes.root,
367
- props.root?.class
368
- )}
362
+ class={cls('root', layerClass('tooltip-root'), classes.root, props.root?.class)}
363
+ class:pointer-events-none={!pointerEvents}
369
364
  style:top="{motionY.current}px"
370
365
  style:left="{motionX.current}px"
371
366
  transition:fade={{ duration: 100 }}
@@ -408,3 +403,15 @@
408
403
  </div>
409
404
  </div>
410
405
  {/if}
406
+
407
+ <style>
408
+ .root {
409
+ position: absolute;
410
+ z-index: 50;
411
+ user-select: none;
412
+ }
413
+
414
+ .pointer-events-none {
415
+ pointer-events: none;
416
+ }
417
+ </style>
@@ -6,13 +6,15 @@
6
6
  const _TooltipContext = new Context<TooltipContextValue>('TooltipContext');
7
7
 
8
8
  type TooltipMode =
9
- | 'bisect-x'
10
- | 'bisect-y'
9
+ | 'bisect-x' // requires values to be sorted
10
+ | 'bisect-y' // requires values to be sorted
11
11
  | 'band'
12
- | 'bisect-band'
12
+ | 'bisect-band' // requires values to be sorted
13
13
  | 'bounds'
14
14
  | 'voronoi'
15
15
  | 'quadtree'
16
+ | 'quadtree-x' // ignores y values (constant 0)
17
+ | 'quadtree-y' // ignores x values (constant 0)
16
18
  | 'manual';
17
19
 
18
20
  export type TooltipContextValue<T = any> = {
@@ -20,7 +22,11 @@
20
22
  y: number;
21
23
  data: T | null;
22
24
  payload: TooltipPayload[];
23
- show(e: PointerEvent, tooltipData?: any, payload?: TooltipPayload): void;
25
+ show(
26
+ e: PointerEvent | MouseEvent | TouchEvent,
27
+ tooltipData?: any,
28
+ payload?: TooltipPayload
29
+ ): void;
24
30
  hide(e?: PointerEvent): void;
25
31
  mode: TooltipMode;
26
32
  isHoveringTooltipArea: boolean;
@@ -72,7 +78,7 @@
72
78
  locked?: boolean;
73
79
 
74
80
  /**
75
- * quadtree search radius
81
+ * quadtree search or voronoi clip radius
76
82
  * @default Infinity
77
83
  */
78
84
  radius?: number;
@@ -116,22 +122,23 @@
116
122
  </script>
117
123
 
118
124
  <script lang="ts" generics="TData = any">
125
+ import type { Snippet } from 'svelte';
119
126
  import { bisector, max, min } from 'd3-array';
120
127
  import { quadtree as d3Quadtree, type Quadtree } from 'd3-quadtree';
121
128
  import { sortFunc, localPoint } from '@layerstack/utils';
122
129
  import { cls } from '@layerstack/tailwind';
123
130
 
131
+ import { getChartContext } from '../Chart.svelte';
132
+ import { getGeoContext } from '../GeoContext.svelte';
124
133
  import Svg from './../layout/Svg.svelte';
125
134
  import Arc from '../Arc.svelte';
126
135
  import ChartClipPath from './../ChartClipPath.svelte';
127
136
  import Voronoi from './../Voronoi.svelte';
128
137
 
129
- import { isScaleBand, scaleInvert } from '../../utils/scales.svelte.js';
138
+ import { isScaleBand, isScaleTime, scaleInvert } from '../../utils/scales.svelte.js';
130
139
  import { cartesianToPolar } from '../../utils/math.js';
131
140
  import { quadtreeRects } from '../../utils/quadtree.js';
132
141
  import { raise } from '../../utils/chart.js';
133
- import { getChartContext } from '../Chart.svelte';
134
- import type { Snippet } from 'svelte';
135
142
  import {
136
143
  getTooltipMetaContext,
137
144
  getTooltipPayload,
@@ -140,6 +147,7 @@
140
147
  import { layerClass } from '../../utils/attributes.js';
141
148
 
142
149
  const ctx = getChartContext<any>();
150
+ const geoCtx = getGeoContext();
143
151
 
144
152
  let {
145
153
  ref: refProp = $bindable(),
@@ -268,7 +276,7 @@
268
276
  }
269
277
  }
270
278
 
271
- function showTooltip(e: PointerEvent, tooltipData?: any) {
279
+ function showTooltip(e: PointerEvent | MouseEvent | TouchEvent, tooltipData?: any) {
272
280
  // Cancel hiding tooltip if from previous event loop
273
281
  if (hideTimeoutId) {
274
282
  clearTimeout(hideTimeoutId);
@@ -296,7 +304,6 @@
296
304
  }
297
305
 
298
306
  // If tooltipData not provided already (voronoi, etc), attempt to find it
299
- // TODO: When using bisect-x/y/band, values should be sorted. Typically they are for `x`, but not `y` (and band depends on if x or y scale)
300
307
  if (tooltipData == null) {
301
308
  switch (mode) {
302
309
  case 'bisect-x': {
@@ -309,6 +316,7 @@
309
316
  xValueAtPoint = scaleInvert(ctx.xScale, point.x - ctx.padding.left);
310
317
  }
311
318
 
319
+ // Requires values to be sorted
312
320
  const index = bisectX(ctx.flatData, xValueAtPoint, 1);
313
321
  const previousValue = ctx.flatData[index - 1];
314
322
  const currentValue = ctx.flatData[index];
@@ -320,6 +328,7 @@
320
328
  // `y` value at pointer coordinate
321
329
  const yValueAtPoint = scaleInvert(ctx.yScale, point.y - ctx.padding.top);
322
330
 
331
+ // Requires values to be sorted
323
332
  const index = bisectY(ctx.flatData, yValueAtPoint, 1);
324
333
  const previousValue = ctx.flatData[index - 1];
325
334
  const currentValue = ctx.flatData[index];
@@ -337,6 +346,7 @@
337
346
  const bandData = ctx.flatData
338
347
  .filter((d) => ctx.x(d) === xValueAtPoint)
339
348
  .sort(sortFunc(ctx.y as () => any)); // sort for bisect
349
+ // Requires values to be sorted
340
350
  const index = bisectY(bandData, yValueAtPoint, 1);
341
351
  const previousValue = bandData[index - 1];
342
352
  const currentValue = bandData[index];
@@ -346,6 +356,7 @@
346
356
  const bandData = ctx.flatData
347
357
  .filter((d) => ctx.y(d) === yValueAtPoint)
348
358
  .sort(sortFunc(ctx.x as () => any)); // sort for bisect
359
+ // Requires values to be sorted
349
360
  const index = bisectX(bandData, xValueAtPoint, 1);
350
361
  const previousValue = bandData[index - 1];
351
362
  const currentValue = bandData[index];
@@ -356,8 +367,14 @@
356
367
  break;
357
368
  }
358
369
 
370
+ case 'quadtree-x':
371
+ case 'quadtree-y':
359
372
  case 'quadtree': {
360
- tooltipData = quadtree?.find(point.x, point.y, radius);
373
+ tooltipData = quadtree?.find(
374
+ point.x - ctx.padding.left,
375
+ point.y - ctx.padding.top,
376
+ radius
377
+ );
361
378
  break;
362
379
  }
363
380
  }
@@ -400,13 +417,20 @@
400
417
  }
401
418
 
402
419
  const quadtree: Quadtree<[number, number]> | undefined = $derived.by(() => {
403
- if (mode === 'quadtree') {
420
+ if (['quadtree', 'quadtree-x', 'quadtree-y'].includes(mode)) {
404
421
  return d3Quadtree()
405
- .extent([
406
- [0, 0],
407
- [ctx.width, ctx.height],
408
- ])
409
422
  .x((d) => {
423
+ if (mode === 'quadtree-y') {
424
+ return 0;
425
+ }
426
+
427
+ if (geoCtx.projection) {
428
+ const lat = ctx.x(d);
429
+ const long = ctx.y(d);
430
+ const geoValue = geoCtx.projection([lat, long]) ?? [0, 0];
431
+ return geoValue[0];
432
+ }
433
+
410
434
  const value = ctx.xGet(d);
411
435
 
412
436
  if (Array.isArray(value)) {
@@ -420,6 +444,17 @@
420
444
  }
421
445
  })
422
446
  .y((d) => {
447
+ if (mode === 'quadtree-x') {
448
+ return 0;
449
+ }
450
+
451
+ if (geoCtx.projection) {
452
+ const lat = ctx.x(d);
453
+ const long = ctx.y(d);
454
+ const geoValue = geoCtx.projection([lat, long]) ?? [0, 0];
455
+ return geoValue[1];
456
+ }
457
+
423
458
  const value = ctx.yGet(d);
424
459
 
425
460
  if (Array.isArray(value)) {
@@ -458,14 +493,63 @@
458
493
  const fullHeight = max(ctx.yRange) - min(ctx.yRange);
459
494
 
460
495
  if (mode === 'band') {
461
- // full band width/height regardless of value
462
- return {
463
- x: isScaleBand(ctx.xScale) ? x - xOffset : min(ctx.xRange),
464
- y: isScaleBand(ctx.yScale) ? y - yOffset : min(ctx.yRange),
465
- width: isScaleBand(ctx.xScale) ? ctx.xScale.step() : fullWidth,
466
- height: isScaleBand(ctx.yScale) ? ctx.yScale.step() : fullHeight,
467
- data: d,
468
- };
496
+ if (isScaleBand(ctx.xScale)) {
497
+ // full band width/height regardless of value
498
+ return {
499
+ x: x - xOffset,
500
+ y: min(ctx.yRange),
501
+ width: ctx.xScale.step(),
502
+ height: fullHeight,
503
+ data: d,
504
+ };
505
+ } else if (isScaleBand(ctx.yScale)) {
506
+ return {
507
+ x: min(ctx.xRange),
508
+ y: y - yOffset,
509
+ width: fullWidth,
510
+ height: ctx.yScale.step(),
511
+ data: d,
512
+ };
513
+ } else if (isScaleTime(ctx.xScale)) {
514
+ // Find width to next data point
515
+ const index = ctx.flatData.findIndex(
516
+ (d2) => Number(ctx.x(d2)) === Number(ctx.x(d))
517
+ );
518
+ const isLastPoint = index + 1 === ctx.flatData.length;
519
+ const nextDataPoint = isLastPoint
520
+ ? max(ctx.xDomain)
521
+ : ctx.x(ctx.flatData[index + 1]);
522
+
523
+ return {
524
+ x: x - xOffset,
525
+ y: min(ctx.yRange),
526
+ width: (ctx.xScale(nextDataPoint) ?? 0) - (xValue ?? 0),
527
+ height: fullHeight,
528
+ data: d,
529
+ };
530
+ } else if (isScaleTime(ctx.yScale)) {
531
+ // Find height to next data point
532
+ const index = ctx.flatData.findIndex(
533
+ (d2) => Number(ctx.y(d2)) === Number(ctx.y(d))
534
+ );
535
+ const isLastPoint = index + 1 === ctx.flatData.length;
536
+ const nextDataPoint = isLastPoint
537
+ ? max(ctx.yDomain)
538
+ : ctx.y(ctx.flatData[index + 1]);
539
+
540
+ return {
541
+ x: min(ctx.xRange),
542
+ y: y - yOffset,
543
+ width: fullWidth,
544
+ height: (ctx.yScale(nextDataPoint) ?? 0) - (yValue ?? 0),
545
+ data: d,
546
+ };
547
+ } else {
548
+ console.warn(
549
+ '[layerchart] TooltipContext band mode requires at least one scale to be band or time.'
550
+ );
551
+ return undefined;
552
+ }
469
553
  } else if (mode === 'bounds') {
470
554
  return {
471
555
  x: isScaleBand(ctx.xScale) || Array.isArray(xValue) ? x - xOffset : min(ctx.xRange),
@@ -493,8 +577,26 @@
493
577
  });
494
578
 
495
579
  const triggerPointerEvents = $derived(
496
- ['bisect-x', 'bisect-y', 'bisect-band', 'quadtree'].includes(mode)
580
+ ['bisect-x', 'bisect-y', 'bisect-band', 'quadtree', 'quadtree-x', 'quadtree-y'].includes(mode)
497
581
  );
582
+
583
+ function onPointerEnter(e: PointerEvent | MouseEvent | TouchEvent) {
584
+ isHoveringTooltipArea = true;
585
+ if (triggerPointerEvents) {
586
+ showTooltip(e);
587
+ }
588
+ }
589
+
590
+ function onPointerMove(e: PointerEvent | MouseEvent | TouchEvent) {
591
+ if (triggerPointerEvents) {
592
+ showTooltip(e);
593
+ }
594
+ }
595
+
596
+ function onPointerLeave(e: PointerEvent | MouseEvent | TouchEvent) {
597
+ isHoveringTooltipArea = false;
598
+ hideTooltip();
599
+ }
498
600
  </script>
499
601
 
500
602
  <!-- svelte-ignore a11y_no_static_element_interactions -->
@@ -505,25 +607,15 @@
505
607
  style:height="{ctx.height}px"
506
608
  class={cls(
507
609
  layerClass('tooltip-context'),
508
- 'absolute touch-none',
610
+ 'absolute',
509
611
  debug && triggerPointerEvents && 'bg-danger/10 outline outline-danger'
510
612
  )}
511
- onpointerenter={(e) => {
512
- isHoveringTooltipArea = true;
513
- if (triggerPointerEvents) {
514
- showTooltip(e);
515
- }
516
- }}
517
- onpointermove={(e) => {
518
- if (triggerPointerEvents) {
519
- showTooltip(e);
520
- }
521
- }}
522
- onpointerleave={(e) => {
523
- isHoveringTooltipArea = false;
524
-
525
- hideTooltip();
526
- }}
613
+ onmouseenter={onPointerEnter}
614
+ ontouchstart={onPointerEnter}
615
+ onmousemove={onPointerMove}
616
+ ontouchmove={onPointerMove}
617
+ onmouseleave={onPointerLeave}
618
+ ontouchend={onPointerLeave}
527
619
  onclick={(e) => {
528
620
  // Ignore clicks without data (triggered from Legend clicks, for example)
529
621
  if (triggerPointerEvents && tooltipContext.data != null) {
@@ -546,6 +638,7 @@
546
638
  {#if mode === 'voronoi'}
547
639
  <Svg>
548
640
  <Voronoi
641
+ r={radius}
549
642
  onpointerenter={(e, { data }) => {
550
643
  showTooltip(e, data);
551
644
  }}
@@ -621,7 +714,7 @@
621
714
  {/each}
622
715
  </g>
623
716
  </Svg>
624
- {:else if mode === 'quadtree' && debug}
717
+ {:else if ['quadtree', 'quadtree-x', 'quadtree-y'].includes(mode) && debug}
625
718
  <Svg pointerEvents={false}>
626
719
  <ChartClipPath>
627
720
  <g class={layerClass('tooltip-quadtree-g')}>
@@ -1,12 +1,12 @@
1
1
  import type { HTMLAttributes } from 'svelte/elements';
2
2
  import type { Without } from '../../utils/types.js';
3
- type TooltipMode = 'bisect-x' | 'bisect-y' | 'band' | 'bisect-band' | 'bounds' | 'voronoi' | 'quadtree' | 'manual';
3
+ type TooltipMode = 'bisect-x' | 'bisect-y' | 'band' | 'bisect-band' | 'bounds' | 'voronoi' | 'quadtree' | 'quadtree-x' | 'quadtree-y' | 'manual';
4
4
  export type TooltipContextValue<T = any> = {
5
5
  x: number;
6
6
  y: number;
7
7
  data: T | null;
8
8
  payload: TooltipPayload[];
9
- show(e: PointerEvent, tooltipData?: any, payload?: TooltipPayload): void;
9
+ show(e: PointerEvent | MouseEvent | TouchEvent, tooltipData?: any, payload?: TooltipPayload): void;
10
10
  hide(e?: PointerEvent): void;
11
11
  mode: TooltipMode;
12
12
  isHoveringTooltipArea: boolean;
@@ -36,7 +36,7 @@ type TooltipContextPropsWithoutHTML<T = any> = {
36
36
  */
37
37
  locked?: boolean;
38
38
  /**
39
- * quadtree search radius
39
+ * quadtree search or voronoi clip radius
40
40
  * @default Infinity
41
41
  */
42
42
  radius?: number;
@@ -1,7 +1,7 @@
1
1
  <script lang="ts" module>
2
2
  import type { HTMLAttributes } from 'svelte/elements';
3
3
  import type { Snippet } from 'svelte';
4
- import type { Without } from '../../utils/types.js';
4
+ import { asAny, type Without } from '../../utils/types.js';
5
5
 
6
6
  export type TooltipHeaderPropsWithoutHTML = {
7
7
  /**
@@ -13,7 +13,7 @@
13
13
  /**
14
14
  * The format to use when displaying the value.
15
15
  */
16
- format?: FormatType;
16
+ format?: FormatType | FormatConfig;
17
17
 
18
18
  /**
19
19
  * The color to use for the color dot.
@@ -54,7 +54,7 @@
54
54
  </script>
55
55
 
56
56
  <script lang="ts">
57
- import { format as formatUtil, type FormatType } from '@layerstack/utils';
57
+ import { format as formatUtil, type FormatType, type FormatConfig } from '@layerstack/utils';
58
58
  import { cls } from '@layerstack/tailwind';
59
59
  import { layerClass } from '../../utils/attributes.js';
60
60
 
@@ -114,6 +114,7 @@
114
114
  {#if children}
115
115
  {@render children?.()}
116
116
  {:else}
117
- {format ? formatUtil(value, format) : value}
117
+ <!-- @ts-expect-error - improve types -->
118
+ {format ? formatUtil(value, asAny(format)) : value}
118
119
  {/if}
119
120
  </div>
@@ -1,6 +1,6 @@
1
1
  import type { HTMLAttributes } from 'svelte/elements';
2
2
  import type { Snippet } from 'svelte';
3
- import type { Without } from '../../utils/types.js';
3
+ import { type Without } from '../../utils/types.js';
4
4
  export type TooltipHeaderPropsWithoutHTML = {
5
5
  /**
6
6
  * The value to display in the tooltip header when the `children`
@@ -10,7 +10,7 @@ export type TooltipHeaderPropsWithoutHTML = {
10
10
  /**
11
11
  * The format to use when displaying the value.
12
12
  */
13
- format?: FormatType;
13
+ format?: FormatType | FormatConfig;
14
14
  /**
15
15
  * The color to use for the color dot.
16
16
  */
@@ -40,7 +40,7 @@ export type TooltipHeaderPropsWithoutHTML = {
40
40
  children?: Snippet;
41
41
  };
42
42
  export type TooltipHeaderProps = TooltipHeaderPropsWithoutHTML & Without<HTMLAttributes<HTMLElement>, TooltipHeaderPropsWithoutHTML>;
43
- import { type FormatType } from '@layerstack/utils';
43
+ import { type FormatType, type FormatConfig } from '@layerstack/utils';
44
44
  declare const TooltipHeader: import("svelte").Component<TooltipHeaderProps, {}, "ref" | "colorRef">;
45
45
  type TooltipHeader = ReturnType<typeof TooltipHeader>;
46
46
  export default TooltipHeader;