layerchart 0.12.1 → 0.13.0

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 (35) hide show
  1. package/components/Arc.svelte +8 -1
  2. package/components/Arc.svelte.d.ts +2 -0
  3. package/components/Area.svelte +3 -0
  4. package/components/Area.svelte.d.ts +4 -0
  5. package/components/AreaStack.svelte +3 -3
  6. package/components/Chart.svelte +9 -1
  7. package/components/Chart.svelte.d.ts +3 -0
  8. package/components/Circle.svelte +9 -1
  9. package/components/Circle.svelte.d.ts +4 -0
  10. package/components/CircleClipPath.svelte +1 -1
  11. package/components/CircleClipPath.svelte.d.ts +2 -0
  12. package/components/Group.svelte +1 -1
  13. package/components/Group.svelte.d.ts +2 -0
  14. package/components/HighlightLine.svelte +78 -60
  15. package/components/HighlightLine.svelte.d.ts +5 -2
  16. package/components/HighlightRect.svelte +50 -45
  17. package/components/HighlightRect.svelte.d.ts +0 -1
  18. package/components/Line.svelte +10 -1
  19. package/components/Line.svelte.d.ts +4 -0
  20. package/components/Path.svelte +10 -1
  21. package/components/Path.svelte.d.ts +4 -0
  22. package/components/Pie.svelte +6 -0
  23. package/components/Pie.svelte.d.ts +4 -0
  24. package/components/RectClipPath.svelte +1 -1
  25. package/components/RectClipPath.svelte.d.ts +2 -0
  26. package/components/Sankey.svelte.d.ts +1 -1
  27. package/components/Tooltip.svelte +39 -343
  28. package/components/Tooltip.svelte.d.ts +2 -13
  29. package/components/TooltipContext.svelte +351 -0
  30. package/components/TooltipContext.svelte.d.ts +46 -0
  31. package/components/index.d.ts +1 -1
  32. package/components/index.js +1 -1
  33. package/package.json +26 -26
  34. package/components/TooltipContainer.svelte +0 -18
  35. package/components/TooltipContainer.svelte.d.ts +0 -19
@@ -1,370 +1,66 @@
1
- <script>import { getContext, createEventDispatcher } from 'svelte';
1
+ <script>import { getContext } from 'svelte';
2
2
  import { spring } from 'svelte/motion';
3
3
  import { fade } from 'svelte/transition';
4
4
  import { writable } from 'svelte/store';
5
- import { bisector, max, min } from 'd3-array';
6
- import { Delaunay } from 'd3-delaunay';
7
- import { quadtree as d3Quadtree } from 'd3-quadtree';
8
- import { Svg, Html } from './Chart.svelte';
9
- import ChartClipPath from './ChartClipPath.svelte';
10
- import { localPoint } from '../utils/event';
11
- import { isScaleBand, scaleInvert } from '../utils/scales';
12
- import { quadtreeRects } from '../utils/quadtree';
13
- import { createPropertySortFunc, createSortFunc } from 'svelte-ux/utils/sort';
14
- const dispatch = createEventDispatcher();
15
- const { flatData, x, xScale, xGet, xRange, y, yScale, yGet, yRange, width, height, padding } = getContext('LayerCake');
16
- /*
17
- TODO: Defaults to consider (if possible to detect scale type, which might not be possible)
18
- - scaleTime / scaleLinear: bisect
19
- - scaleTime / scaleLinear (multi/stack): bisect
20
- - scaleTime / scaleBand: bisect (or band)
21
- - scaleTime (multi) / scaleBand: bounds (or possible band if not overlapping)
22
- - scaleBand, scaleLinear: band (or bounds)
23
- - scaleBand, scaleLinear: band (or bounds) - multiple (overlapping) bars
24
- - scaleLinear, scaleLinear: voronoi (or quadtree)
25
- */
26
- export let mode = 'bisect-x';
27
- export let snapToDataX = false;
28
- export let snapToDataY = false;
29
- export let findTooltipData = 'closest';
5
+ import { tooltipContext } from './TooltipContext.svelte';
30
6
  export let topOffset = 10;
31
7
  export let leftOffset = 10;
32
8
  export let contained = 'container'; // TODO: Support 'window' using getBoundingClientRect()
33
9
  export let animate = true;
34
- export let radius = Infinity;
35
- export let debug = false;
36
- let tooltip = null;
10
+ export let header = undefined;
11
+ const { width, height, padding } = getContext('LayerCake');
12
+ const tooltip = tooltipContext();
37
13
  let tooltipWidth = 0;
38
14
  let tooltipHeight = 0;
39
- let top = animate ? spring(0) : writable(0);
40
- $: if (tooltip) {
15
+ let top = animate ? spring($tooltip.top) : writable($tooltip.top);
16
+ $: if ($tooltip) {
41
17
  const containerHeight = $height + $padding.bottom;
42
- if (contained === 'container' && tooltip.top + topOffset + tooltipHeight > containerHeight) {
18
+ if (contained === 'container' && $tooltip.top + topOffset + tooltipHeight > containerHeight) {
43
19
  // change side
44
- $top = tooltip.top - (topOffset + tooltipHeight);
20
+ $top = $tooltip.top - (topOffset + tooltipHeight);
45
21
  }
46
22
  else {
47
- $top = tooltip.top + topOffset;
23
+ $top = $tooltip.top + topOffset;
48
24
  }
49
25
  }
50
- let left = animate ? spring(0) : writable(0);
51
- $: if (tooltip) {
26
+ let left = animate ? spring($tooltip.left) : writable($tooltip.left);
27
+ $: if ($tooltip) {
52
28
  const containerWidth = $width + $padding.right;
53
- if (contained === 'container' && tooltip.left + leftOffset + tooltipWidth > containerWidth) {
29
+ if (contained === 'container' && $tooltip.left + leftOffset + tooltipWidth > containerWidth) {
54
30
  // change side
55
- $left = tooltip.left - (leftOffset + tooltipWidth);
31
+ $left = $tooltip.left - (leftOffset + tooltipWidth);
56
32
  }
57
33
  else {
58
- $left = tooltip.left + leftOffset;
34
+ $left = $tooltip.left + leftOffset;
59
35
  }
60
36
  }
61
- $: bisectX = bisector((d) => {
62
- const value = $x(d);
63
- if (Array.isArray(value)) {
64
- // `x` accessor with multiple properties (ex. `x={['start', 'end']})`)
65
- // Using first value. Consider using average, max, etc
66
- // const midpoint = new Date((value[1].valueOf() + value[0].getTime()) / 2);
67
- // return midpoint;
68
- return value[0];
69
- }
70
- else {
71
- return value;
72
- }
73
- }).left;
74
- $: bisectY = bisector((d) => {
75
- const value = $y(d);
76
- if (Array.isArray(value)) {
77
- // `x` accessor with multiple properties (ex. `x={['start', 'end']})`)
78
- // Using first value. Consider using average, max, etc
79
- // const midpoint = new Date((value[1].valueOf() + value[0].getTime()) / 2);
80
- // return midpoint;
81
- return value[0];
82
- }
83
- else {
84
- return value;
85
- }
86
- }).left;
87
- function findData(previousValue, currentValue, valueAtPoint, accessor) {
88
- switch (findTooltipData) {
89
- case 'closest':
90
- if (currentValue === undefined) {
91
- return previousValue;
92
- }
93
- else if (previousValue === undefined) {
94
- return currentValue;
95
- }
96
- else {
97
- return Number(valueAtPoint) - Number(accessor(previousValue)) >
98
- Number(accessor(currentValue)) - Number(valueAtPoint)
99
- ? currentValue
100
- : previousValue;
101
- }
102
- case 'left':
103
- return previousValue;
104
- case 'right':
105
- default:
106
- return currentValue;
107
- }
108
- }
109
- function handleTooltip(event, tooltipData) {
110
- const referenceNode = event.target.closest('.layercake-container');
111
- const point = localPoint(referenceNode, event);
112
- const localX = point?.x - $padding.left ?? 0;
113
- const localY = point?.y - $padding.top ?? 0;
114
- // If tooltipData not provided already (voronoi, etc), attempt to find it
115
- // TODO: When using bisect-x/y/band, values should be sorted. Tyipcally are for `x`, but not `y` (and band depends on if x or y scale)
116
- if (tooltipData == null) {
117
- if (mode === 'quadtree') {
118
- tooltipData = quadtree.find(localX, localY, radius);
119
- }
120
- else if (mode === 'bisect-band') {
121
- // `x` and `y` values at mouse/touch coordinate
122
- const xValueAtPoint = scaleInvert($xScale, localX);
123
- const yValueAtPoint = scaleInvert($yScale, localY);
124
- if (isScaleBand($xScale)) {
125
- // Find point closest to pointer within the x band
126
- const bandData = $flatData
127
- .filter((d) => $x(d) === xValueAtPoint)
128
- .sort(createSortFunc($y)); // sort for bisect
129
- const index = bisectY(bandData, yValueAtPoint, 1);
130
- const previousValue = bandData[index - 1];
131
- const currentValue = bandData[index];
132
- tooltipData = findData(previousValue, currentValue, yValueAtPoint, $y);
133
- }
134
- else if (isScaleBand($yScale)) {
135
- // Find point closest to pointer within the y band
136
- const bandData = $flatData
137
- .filter((d) => $y(d) === yValueAtPoint)
138
- .sort(createSortFunc($x)); // sort for bisect
139
- const index = bisectX(bandData, xValueAtPoint, 1);
140
- const previousValue = bandData[index - 1];
141
- const currentValue = bandData[index];
142
- tooltipData = findData(previousValue, currentValue, xValueAtPoint, $x);
143
- }
144
- else {
145
- // TODO: Support `bisect-band` without band? Fallback to bisect?
146
- }
147
- }
148
- else if (mode === 'bisect-x') {
149
- // `x` value at mouse/touch coordinate
150
- const xValueAtPoint = scaleInvert($xScale, localX);
151
- const index = bisectX($flatData, xValueAtPoint, 1);
152
- const previousValue = $flatData[index - 1];
153
- const currentValue = $flatData[index];
154
- tooltipData = findData(previousValue, currentValue, xValueAtPoint, $x);
155
- }
156
- else if (mode === 'bisect-y') {
157
- // `y` value at mouse/touch coordinate
158
- const yValueAtPoint = scaleInvert($yScale, localY);
159
- const index = bisectY($flatData, yValueAtPoint, 1);
160
- const previousValue = $flatData[index - 1];
161
- const currentValue = $flatData[index];
162
- tooltipData = findData(previousValue, currentValue, yValueAtPoint, $y);
163
- }
164
- }
165
- if (tooltipData) {
166
- tooltip = {
167
- left: snapToDataX ? $xGet(tooltipData) : localX,
168
- top: snapToDataY ? $yGet(tooltipData) : localY,
169
- data: tooltipData
170
- };
171
- }
172
- else {
173
- // Hide tooltip if unable to locate
174
- tooltip = null;
175
- }
176
- }
177
- function hideTooltip(event) {
178
- tooltip = null;
179
- }
180
- let points;
181
- let voronoi;
182
- $: if (mode === 'voronoi') {
183
- points = $flatData.map((d) => {
184
- const xValue = $xGet(d);
185
- const yValue = $yGet(d);
186
- const x = Array.isArray(xValue) ? min(xValue) : xValue;
187
- const y = Array.isArray(yValue) ? min(yValue) : yValue;
188
- const point = [x, y];
189
- point.data = d;
190
- return point;
191
- });
192
- voronoi = Delaunay.from(points).voronoi([0, 0, Math.max($width, 0), Math.max($height, 0)]); // width and/or height can sometimes be negative (when loading data remotely and updately)
193
- }
194
- let quadtree;
195
- $: if (mode === 'quadtree') {
196
- quadtree = d3Quadtree()
197
- .extent([
198
- [0, 0],
199
- [$width, $height]
200
- ])
201
- .x((d) => {
202
- const value = $xGet(d);
203
- if (Array.isArray(value)) {
204
- // `x` accessor with multiple properties (ex. `x={['start', 'end']})`)
205
- // Using first value. Consider using average, max, etc
206
- // const midpoint = new Date((value[1].valueOf() + value[0].getTime()) / 2);
207
- // return midpoint;
208
- return min(value);
209
- }
210
- else {
211
- return value;
212
- }
213
- })
214
- .y((d) => {
215
- const value = $yGet(d);
216
- if (Array.isArray(value)) {
217
- // `x` accessor with multiple properties (ex. `x={['start', 'end']})`)
218
- // Using first value. Consider using average, max, etc
219
- // const midpoint = new Date((value[1].valueOf() + value[0].getTime()) / 2);
220
- // return midpoint;
221
- return min(value);
222
- }
223
- else {
224
- return value;
225
- }
226
- })
227
- .addAll($flatData);
228
- }
229
- let rects = [];
230
- $: if (mode === 'bounds' || mode === 'band') {
231
- rects = $flatData
232
- .map((d) => {
233
- const xValue = $xGet(d);
234
- const yValue = $yGet(d);
235
- const x = Array.isArray(xValue) ? min(xValue) : xValue;
236
- const y = Array.isArray(yValue) ? max(yValue) : yValue;
237
- const xOffset = isScaleBand($xScale) ? ($xScale.padding() * $xScale.step()) / 2 : 0;
238
- const yOffset = isScaleBand($yScale) ? ($yScale.padding() * $yScale.step()) / 2 : 0;
239
- const fullWidth = max($xRange) - min($xRange);
240
- const fullHeight = max($yRange) - min($yRange);
241
- if (mode === 'band') {
242
- // full band width/height regardless of value
243
- return {
244
- x: isScaleBand($xScale) ? x - xOffset : min($xRange),
245
- y: isScaleBand($yScale) ? y - yOffset : min($yRange),
246
- width: isScaleBand($xScale) ? $xScale.step() : fullWidth,
247
- height: isScaleBand($yScale) ? $yScale.step() : fullHeight,
248
- data: d
249
- };
250
- }
251
- else if (mode === 'bounds') {
252
- return {
253
- x: isScaleBand($xScale) || Array.isArray(xValue) ? x - xOffset : min($xRange),
254
- // y: isScaleBand($yScale) || Array.isArray(yValue) ? y - yOffset : min($yRange),
255
- y: y - yOffset,
256
- width: Array.isArray(xValue)
257
- ? xValue[1] - xValue[0]
258
- : isScaleBand($xScale)
259
- ? $xScale.step()
260
- : min($xRange) + x,
261
- height: Array.isArray(yValue)
262
- ? yValue[1] - yValue[0]
263
- : isScaleBand($yScale)
264
- ? $yScale.step()
265
- : max($yRange) - y,
266
- data: d
267
- };
268
- }
269
- })
270
- .sort(createPropertySortFunc('x'));
271
- // console.log({ rects });
272
- }
273
37
  </script>
274
38
 
275
- {#if tooltip}
276
- <Html>
277
- <div
278
- class="absolute pointer-events-none z-50"
279
- style="
280
- top: {$top}px;
281
- left: {$left}px;
282
- max-width: {$width / 2}px;
283
- "
284
- transition:fade={{ duration: 100 }}
285
- bind:clientWidth={tooltipWidth}
286
- bind:clientHeight={tooltipHeight}
287
- >
288
- <slot data={tooltip?.data} />
289
- </div>
290
- </Html>
291
-
292
- <Svg>
293
- <slot name="highlight" data={tooltip?.data} />
294
- </Svg>
295
- {/if}
39
+ {#if $tooltip.data}
40
+ <div
41
+ class="absolute pointer-events-none z-50"
42
+ style:top="{$top}px"
43
+ style:left="{$left}px"
44
+ transition:fade={{ duration: 100 }}
45
+ bind:clientWidth={tooltipWidth}
46
+ bind:clientHeight={tooltipHeight}
47
+ >
48
+ <!-- <slot data={tooltip?.data} /> -->
296
49
 
297
- {#if ['bisect-x', 'bisect-y', 'bisect-band', 'quadtree'].indexOf(mode) != -1}
298
- <Html>
299
50
  <div
300
- class="absolute"
301
- style="width: {$width}px; height: {$height}px; background: _red; z-index: 9999"
302
- on:touchstart={handleTooltip}
303
- on:touchmove={handleTooltip}
304
- on:mousemove={handleTooltip}
305
- on:mouseleave={hideTooltip}
306
- on:click={(e) => {
307
- dispatch('click', { data: tooltip?.data });
308
- }}
309
- />
310
- </Html>
311
- {:else if mode === 'voronoi'}
312
- <Svg>
313
- {#each points as point, i}
314
- <g class="tooltip-voronoi">
315
- <path
316
- d={voronoi.renderCell(i)}
317
- style:fill={debug ? 'red' : 'transparent'}
318
- style:fill-opacity={debug ? 0.1 : 0}
319
- style:stroke={debug ? 'red' : 'transparent'}
320
- on:mousemove={(e) => handleTooltip(e, point.data)}
321
- on:mouseleave={hideTooltip}
322
- on:click={(e) => {
323
- dispatch('click', { data: point.data });
324
- }}
325
- />
326
- </g>
327
- {/each}
328
- </Svg>
329
- {:else if mode === 'bounds' || mode === 'band'}
330
- <Svg>
331
- <g class="tooltip-rects">
332
- {#each rects as rect}
333
- <rect
334
- x={rect.x}
335
- y={rect.y}
336
- width={rect.width}
337
- height={rect.height}
338
- style:fill={debug ? 'red' : 'transparent'}
339
- style:fill-opacity={debug ? 0.1 : 0}
340
- style:stroke={debug ? 'red' : 'transparent'}
341
- on:mousemove={(e) => handleTooltip(e, rect.data)}
342
- on:mouseleave={hideTooltip}
343
- on:click={(e) => {
344
- dispatch('click', { data: rect.data });
345
- }}
346
- />
347
- {/each}
348
- </g>
349
- </Svg>
350
- {/if}
51
+ class="bg-gray-900/90 backdrop-filter backdrop-blur-[2px] text-white rounded elevation-1 px-2 py-1"
52
+ >
53
+ {#if header || $$slots.header}
54
+ <div class="text-center font-semibold pb-1 whitespace-nowrap">
55
+ <slot name="header">
56
+ {header($tooltip.data)}
57
+ </slot>
58
+ </div>
59
+ {/if}
351
60
 
352
- {#if mode === 'quadtree' && debug}
353
- <Svg>
354
- <ChartClipPath>
355
- <g class="tooltip-quadtree">
356
- {#each quadtreeRects(quadtree, false) as rect}
357
- <rect
358
- x={rect.x}
359
- y={rect.y}
360
- width={rect.width}
361
- height={rect.height}
362
- style:fill={debug ? 'red' : 'transparent'}
363
- style:fill-opacity={debug ? 0.1 : 0}
364
- stroke="red"
365
- />
366
- {/each}
367
- </g>
368
- </ChartClipPath>
369
- </Svg>
61
+ <div class="grid grid-cols-[1fr,auto] gap-x-2 gap-y-1 items-center">
62
+ <slot data={$tooltip.data} />
63
+ </div>
64
+ </div>
65
+ </div>
370
66
  {/if}
@@ -1,31 +1,20 @@
1
1
  import { SvelteComponentTyped } from "svelte";
2
2
  declare const __propDef: {
3
3
  props: {
4
- mode?: 'bisect-x' | 'bisect-y' | 'band' | 'bisect-band' | 'bounds' | 'voronoi' | 'quadtree';
5
- snapToDataX?: boolean;
6
- snapToDataY?: boolean;
7
- findTooltipData?: 'closest' | 'left' | 'right';
8
4
  topOffset?: number;
9
5
  leftOffset?: number;
10
6
  contained?: 'container' | false;
11
7
  animate?: boolean;
12
- radius?: number;
13
- debug?: boolean;
8
+ header?: (data: any) => any;
14
9
  };
15
10
  events: {
16
- click: CustomEvent<{
17
- data: any;
18
- }>;
19
- } & {
20
11
  [evt: string]: CustomEvent<any>;
21
12
  };
22
13
  slots: {
14
+ header: {};
23
15
  default: {
24
16
  data: any;
25
17
  };
26
- highlight: {
27
- data: any;
28
- };
29
18
  };
30
19
  };
31
20
  export type TooltipProps = typeof __propDef.props;