layerchart 0.6.3 → 0.6.6

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.
@@ -20,10 +20,7 @@ $: tickVals = Array.isArray(ticks)
20
20
 
21
21
  <g class="axis y-axis" transform="translate({-$padding.left}, 0)">
22
22
  {#each tickVals as tick, i}
23
- <g
24
- class="tick tick-{tick}"
25
- transform="translate({$xRange[0] + (isBand ? $padding.left : 0)}, {$yScale(tick)})"
26
- >
23
+ <g class="tick tick-{tick}" transform="translate({$xRange[0]}, {$yScale(tick)})">
27
24
  {#if gridlines !== false}
28
25
  <line
29
26
  x1={$padding.left}
@@ -1,4 +1,5 @@
1
- <script>import { getContext } from 'svelte';
1
+ <script>import { isScaleBand } from '../utils/scales';
2
+ import { getContext } from 'svelte';
2
3
  import { get } from 'svelte/store';
3
4
  import Circle from './Circle.svelte';
4
5
  import Line from './Line.svelte';
@@ -10,33 +11,68 @@ $: x = $xGet(data);
10
11
  function getColor(index) {
11
12
  return color ?? get(zScale)(index) ?? 'var(--color-blue-500)';
12
13
  }
13
- $: points = Array.isArray(data)
14
- ? // Stack series
15
- data.map((yValue, i) => ({
16
- x,
17
- y: $yScale(yValue),
18
- color: getColor(i)
19
- }))
20
- : [
14
+ let lines = [];
15
+ $: if (Array.isArray(x)) {
16
+ // `x` accessor with multiple properties (ex. `x={['start', 'end']})`)
17
+ lines = x.map((xItem, i) => ({
18
+ x1: xItem,
19
+ y1: 0,
20
+ x2: xItem,
21
+ y2: $yRange[0]
22
+ }));
23
+ }
24
+ else {
25
+ lines = [
26
+ {
27
+ x1: x,
28
+ y1: 0,
29
+ x2: x,
30
+ y2: $yRange[0]
31
+ }
32
+ ];
33
+ }
34
+ let points = [];
35
+ $: yOffset = isScaleBand($yScale) ? $yScale.bandwidth() / 2 : 0;
36
+ $: if (Array.isArray(x)) {
37
+ // `x` accessor with multiple properties (ex. `x={['start', 'end']})`)
38
+ points = x.map((xItem, i) => ({
39
+ x: xItem,
40
+ y: $yGet(data) + yOffset,
41
+ color: getColor(i)
42
+ }));
43
+ }
44
+ else if (Array.isArray(data)) {
45
+ // Stack series
46
+ points = data.map((yValue, i) => ({
47
+ x,
48
+ y: $yScale(yValue) + yOffset,
49
+ color: getColor(i)
50
+ }));
51
+ }
52
+ else {
53
+ points = [
21
54
  {
22
55
  x,
23
- y: $yGet(data) - $padding.top,
56
+ y: $yGet(data) + yOffset,
24
57
  color: getColor(0)
25
58
  }
26
59
  ];
60
+ }
27
61
  </script>
28
62
 
29
- <Line
30
- spring
31
- x1={x}
32
- y1={0}
33
- x2={x}
34
- y2={$yRange[0]}
35
- stroke="rgba(0,0,0,.5)"
36
- stroke-width={2}
37
- style="pointerEvents: none"
38
- stroke-dasharray="2,2"
39
- />
63
+ {#each lines as line}
64
+ <Line
65
+ spring
66
+ x1={line.x1}
67
+ y1={line.y1}
68
+ x2={line.x2}
69
+ y2={line.y2}
70
+ stroke="rgba(0,0,0,.5)"
71
+ stroke-width={2}
72
+ style="pointerEvents: none"
73
+ stroke-dasharray="2,2"
74
+ />
75
+ {/each}
40
76
 
41
77
  {#each points as point}
42
78
  <Circle
@@ -1,22 +1,30 @@
1
1
  <script>import { getContext } from 'svelte';
2
+ import { max, min } from 'd3-array';
2
3
  import { isScaleBand } from '../utils/scales';
3
4
  import Rect from './Rect.svelte';
4
5
  export let data;
5
6
  const { flatData, xScale, x, xGet, yRange, padding } = getContext('LayerCake');
6
7
  $: isBand = isScaleBand($xScale);
8
+ $: xCoord = $xGet(data);
7
9
  let width = 0;
8
10
  $: if (isBand) {
9
11
  width = $xScale.step();
10
12
  }
13
+ else if (Array.isArray(xCoord)) {
14
+ // `x` accessor with multiple properties (ex. `x={['start', 'end']})`)
15
+ // Use first/last values for width
16
+ width = max(xCoord) - min(xCoord);
17
+ xCoord = min(xCoord); // Use left-most value for top left of rect
18
+ }
11
19
  else {
12
20
  // Find width to next data point
13
21
  let index = $flatData.findIndex((d) => Number($x(d)) === Number($x(data)));
14
22
  let nextDataPoint = $x($flatData[index + 1]);
15
- width = ($xScale(nextDataPoint) ?? 0) - ($xGet(data) ?? 0);
23
+ width = ($xScale(nextDataPoint) ?? 0) - (xCoord ?? 0);
16
24
  }
17
25
  $: dimensions = {
18
- x: $xGet(data) - (isBand ? ($xScale.padding() * $xScale.step()) / 2 : 0),
19
- y: -$padding.top,
26
+ x: xCoord - (isBand ? ($xScale.padding() * $xScale.step()) / 2 : 0),
27
+ y: 0,
20
28
  width,
21
29
  height: $yRange[0]
22
30
  };
@@ -2,10 +2,11 @@
2
2
  import Circle from './Circle.svelte';
3
3
  import { isScaleBand } from '../utils/scales';
4
4
  const context = getContext('LayerCake');
5
- const { data, xGet, yGet, xScale, yScale, config } = context;
5
+ const { data, xGet, y, yGet, xScale, yScale, rGet, config } = context;
6
6
  export let r = 5;
7
7
  export let offsetX = undefined;
8
8
  export let offsetY = undefined;
9
+ export let color = 'var(--color-blue-500)';
9
10
  function getOffset(value, offset, scale) {
10
11
  if (typeof offset === 'function') {
11
12
  return offset(value, context);
@@ -20,6 +21,18 @@ function getOffset(value, offset, scale) {
20
21
  return 0;
21
22
  }
22
23
  }
24
+ function getColor(item, index) {
25
+ if (typeof color === 'function') {
26
+ return color({ value: $y(item), item, index });
27
+ }
28
+ else if ($config.r) {
29
+ // console.log({ item, value: $rGet(item), scale: $rGet.domain() });
30
+ return $rGet(item);
31
+ }
32
+ else {
33
+ return color;
34
+ }
35
+ }
23
36
  $: points = $data.flatMap((d) => {
24
37
  if (Array.isArray($config.x)) {
25
38
  /*
@@ -29,7 +42,8 @@ $: points = $data.flatMap((d) => {
29
42
  return $xGet(d).map((x) => {
30
43
  return {
31
44
  x: x + getOffset(x, offsetX, $xScale),
32
- y: $yGet(d) + getOffset($yGet(d), offsetY, $yScale)
45
+ y: $yGet(d) + getOffset($yGet(d), offsetY, $yScale),
46
+ data: d
33
47
  };
34
48
  });
35
49
  }
@@ -41,7 +55,8 @@ $: points = $data.flatMap((d) => {
41
55
  return $yGet(d).map((y) => {
42
56
  return {
43
57
  x: $xGet(d) + getOffset($xGet(d), offsetX, $xScale),
44
- y: y + getOffset(y, offsetY, $yScale)
58
+ y: y + getOffset(y, offsetY, $yScale),
59
+ data: d
45
60
  };
46
61
  });
47
62
  }
@@ -52,7 +67,8 @@ $: points = $data.flatMap((d) => {
52
67
  */
53
68
  return {
54
69
  x: $xGet(d) + getOffset($xGet(d), offsetX, $xScale),
55
- y: $yGet(d) + getOffset($yGet(d), offsetY, $yScale)
70
+ y: $yGet(d) + getOffset($yGet(d), offsetY, $yScale),
71
+ data: d
56
72
  };
57
73
  }
58
74
  });
@@ -60,8 +76,8 @@ $: points = $data.flatMap((d) => {
60
76
 
61
77
  <slot {points}>
62
78
  <g class="point-group">
63
- {#each points as point}
64
- <Circle cx={point.x} cy={point.y} {r} {...$$restProps} />
79
+ {#each points as point, index}
80
+ <Circle cx={point.x} cy={point.y} {r} fill={getColor(point.data, index)} {...$$restProps} />
65
81
  {/each}
66
82
  </g>
67
83
  </slot>
@@ -5,6 +5,11 @@ declare const __propDef: {
5
5
  r?: number;
6
6
  offsetX?: number | ((value: number, context: any) => number);
7
7
  offsetY?: number | ((value: number, context: any) => number);
8
+ color?: string | ((obj: {
9
+ value: any;
10
+ item: any;
11
+ index: number;
12
+ }) => string);
8
13
  };
9
14
  events: {
10
15
  [evt: string]: CustomEvent<any>;
@@ -2,12 +2,27 @@
2
2
  import { spring } from 'svelte/motion';
3
3
  import { fade } from 'svelte/transition';
4
4
  import { writable } from 'svelte/store';
5
- import { bisector } from 'd3-array';
5
+ import { bisector, max, min } from 'd3-array';
6
+ import { Delaunay } from 'd3-delaunay';
7
+ import { quadtree as d3Quadtree } from 'd3-quadtree';
6
8
  import { Svg, Html } from './Chart.svelte';
9
+ import ChartClipPath from './ChartClipPath.svelte';
7
10
  import { localPoint } from '../utils/event';
8
- import { isScaleBand, scaleBandInvert } from '../utils/scales';
11
+ import { isScaleBand, scaleInvert } from '../utils/scales';
12
+ import { quadtreeRects } from '../utils/quadtree';
9
13
  const dispatch = createEventDispatcher();
10
- const { flatData, x, xScale, xGet, yScale, yGet, width, height, padding } = getContext('LayerCake');
14
+ const { flatData, x, xScale, xGet, xRange, yScale, yGet, yRange, width, height, padding } = getContext('LayerCake');
15
+ /*
16
+ TODO: Defaults to consider (if possible to detect scale type, which might not be possible)
17
+ - scaleTime / scaleLinear: bisect
18
+ - scaleTime / scaleLinear (multi/stack): bisect
19
+ - scaleTime / scaleBand: bisect (or band)
20
+ - scaleTime (multi) / scaleBand: bounds (or possible band if not overlapping)
21
+ - scaleBand, scaleLinear: band (or bounds)
22
+ - scaleBand, scaleLinear: band (or bounds) - multiple (overlapping) bars
23
+ - scaleLinear, scaleLinear: voronoi (or quadtree)
24
+ */
25
+ export let mode = 'bisect';
11
26
  export let snapToDataX = false;
12
27
  export let snapToDataY = false;
13
28
  export let findTooltipData = 'closest';
@@ -15,6 +30,8 @@ export let topOffset = 10;
15
30
  export let leftOffset = 10;
16
31
  export let contained = 'container'; // TODO: Support 'window' using getBoundingClientRect()
17
32
  export let animate = true;
33
+ export let radius = Infinity;
34
+ export let debug = false;
18
35
  let tooltip = null;
19
36
  let tooltipWidth = 0;
20
37
  let tooltipHeight = 0;
@@ -40,34 +57,63 @@ $: if (tooltip) {
40
57
  $left = tooltip.left + leftOffset;
41
58
  }
42
59
  }
43
- function handleTooltip(event) {
60
+ function handleTooltip(event, tooltipData) {
44
61
  const point = localPoint(event.target, event);
45
62
  const localX = point?.x ?? 0;
46
63
  const localY = point?.y ?? 0;
47
- let tooltipData;
48
- if (isScaleBand($xScale)) {
49
- // `x` value at mouse coordinate
50
- const xValue = scaleBandInvert($xScale)(localX);
51
- tooltipData = $flatData.find((d) => $x(d) === xValue);
52
- }
53
- else {
54
- // `x` value at mouse coordinate
55
- const xValue = $xScale.invert(localX);
56
- const bisectX = bisector($x).left;
57
- const index = bisectX($flatData, xValue, 1);
58
- const data0 = $flatData[index - 1];
59
- const data1 = $flatData[index];
60
- switch (findTooltipData) {
61
- case 'closest':
62
- tooltipData =
63
- Number(xValue) - Number($x(data0)) > Number($x(data1)) - Number(xValue) ? data1 : data0;
64
- break;
65
- case 'left':
66
- tooltipData = data0;
67
- break;
68
- case 'right':
69
- default:
70
- tooltipData = data1;
64
+ // If tooltipData not provided already (voronoi, etc), attempt to find it
65
+ if (tooltipData == null) {
66
+ if (mode === 'quadtree') {
67
+ tooltipData = quadtree.find(localX, localY, radius);
68
+ }
69
+ else {
70
+ // `x` value at mouse/touch coordinate
71
+ const valueAtPoint = scaleInvert($xScale, localX);
72
+ if (isScaleBand($xScale)) {
73
+ tooltipData = $flatData.find((d) => $x(d) === valueAtPoint);
74
+ }
75
+ else {
76
+ // continuous scale (linear, time, etc). Use bisector to find closest data to mouse location
77
+ const bisectX = bisector((d) => {
78
+ const value = $x(d);
79
+ if (Array.isArray(value)) {
80
+ // `x` accessor with multiple properties (ex. `x={['start', 'end']})`)
81
+ // Using first value. Consider using average, max, etc
82
+ // const midpoint = new Date((value[1].valueOf() + value[0].getTime()) / 2);
83
+ // return midpoint;
84
+ return value[0];
85
+ }
86
+ else {
87
+ return value;
88
+ }
89
+ }).left;
90
+ const index = bisectX($flatData, valueAtPoint, 1);
91
+ const data0 = $flatData[index - 1];
92
+ const data1 = $flatData[index];
93
+ switch (findTooltipData) {
94
+ case 'closest':
95
+ if (data1 === undefined) {
96
+ tooltipData = data0;
97
+ }
98
+ else if (data0 === undefined) {
99
+ tooltipData = data1;
100
+ }
101
+ else {
102
+ tooltipData =
103
+ Number(valueAtPoint) - Number($x(data0)) >
104
+ Number($x(data1)) - Number(valueAtPoint)
105
+ ? data1
106
+ : data0;
107
+ }
108
+ break;
109
+ case 'left':
110
+ tooltipData = data0;
111
+ break;
112
+ case 'right':
113
+ default:
114
+ tooltipData = data1;
115
+ }
116
+ }
71
117
  }
72
118
  }
73
119
  if (tooltipData) {
@@ -77,26 +123,107 @@ function handleTooltip(event) {
77
123
  data: tooltipData
78
124
  };
79
125
  }
126
+ else {
127
+ // Hide tooltip if unable to locate
128
+ tooltip = null;
129
+ }
80
130
  }
81
131
  function hideTooltip(event) {
82
132
  tooltip = null;
83
133
  }
134
+ let points;
135
+ let voronoi;
136
+ $: if (mode === 'voronoi') {
137
+ points = $flatData.map((d) => {
138
+ const xValue = $xGet(d);
139
+ const yValue = $yGet(d);
140
+ const x = Array.isArray(xValue) ? xValue[0] : xValue;
141
+ const y = Array.isArray(yValue) ? yValue[0] : yValue;
142
+ const point = [x, y];
143
+ point.data = d;
144
+ return point;
145
+ });
146
+ voronoi = Delaunay.from(points).voronoi([0, 0, $width, $height]);
147
+ }
148
+ let quadtree;
149
+ $: if (mode === 'quadtree') {
150
+ quadtree = d3Quadtree()
151
+ .extent([
152
+ [0, 0],
153
+ [$width, $height]
154
+ ])
155
+ .x((d) => {
156
+ const value = $xGet(d);
157
+ if (Array.isArray(value)) {
158
+ // `x` accessor with multiple properties (ex. `x={['start', 'end']})`)
159
+ // Using first value. Consider using average, max, etc
160
+ // const midpoint = new Date((value[1].valueOf() + value[0].getTime()) / 2);
161
+ // return midpoint;
162
+ return value[0];
163
+ }
164
+ else {
165
+ return value;
166
+ }
167
+ })
168
+ .y((d) => {
169
+ const value = $yGet(d);
170
+ if (Array.isArray(value)) {
171
+ // `x` accessor with multiple properties (ex. `x={['start', 'end']})`)
172
+ // Using first value. Consider using average, max, etc
173
+ // const midpoint = new Date((value[1].valueOf() + value[0].getTime()) / 2);
174
+ // return midpoint;
175
+ return value[0];
176
+ }
177
+ else {
178
+ return value;
179
+ }
180
+ })
181
+ .addAll($flatData);
182
+ }
183
+ let rects = [];
184
+ $: if (mode === 'bounds' || mode === 'band') {
185
+ rects = $flatData.map((d) => {
186
+ const xValue = $xGet(d);
187
+ const yValue = $yGet(d);
188
+ const x = Array.isArray(xValue) ? min(xValue) : xValue;
189
+ const y = Array.isArray(yValue) ? max(yValue) : yValue;
190
+ const xOffset = isScaleBand($xScale) ? ($xScale.padding() * $xScale.step()) / 2 : 0;
191
+ const yOffset = isScaleBand($yScale) ? ($yScale.padding() * $yScale.step()) / 2 : 0;
192
+ const fullWidth = max($xRange) - min($xRange);
193
+ const fullHeight = max($yRange) - min($yRange);
194
+ if (mode === 'band') {
195
+ // full band width/height regardless of value
196
+ return {
197
+ x: isScaleBand($xScale) ? x - xOffset : min($xRange),
198
+ y: isScaleBand($yScale) ? y - yOffset : min($yRange),
199
+ width: isScaleBand($xScale) ? $xScale.step() : fullWidth,
200
+ height: isScaleBand($yScale) ? $yScale.step() : fullHeight,
201
+ data: d
202
+ };
203
+ }
204
+ else if (mode === 'bounds') {
205
+ return {
206
+ x: isScaleBand($xScale) || Array.isArray(xValue) ? x - xOffset : min($xRange),
207
+ // y: isScaleBand($yScale) || Array.isArray(yValue) ? y - yOffset : min($yRange),
208
+ y: y - yOffset,
209
+ width: Array.isArray(xValue)
210
+ ? xValue[1] - xValue[0]
211
+ : isScaleBand($xScale)
212
+ ? $xScale.step()
213
+ : min($xRange) + x,
214
+ height: Array.isArray(yValue)
215
+ ? yValue[1] - yValue[0]
216
+ : isScaleBand($yScale)
217
+ ? $yScale.step()
218
+ : max($yRange) - y,
219
+ data: d
220
+ };
221
+ }
222
+ });
223
+ // console.log({ rects });
224
+ }
84
225
  </script>
85
226
 
86
- <Html>
87
- <div
88
- class="absolute"
89
- style="width: {$width}px; height: {$height}px; background: _red; z-index: 9999"
90
- on:touchstart={handleTooltip}
91
- on:touchmove={handleTooltip}
92
- on:mousemove={handleTooltip}
93
- on:mouseleave={hideTooltip}
94
- on:click={(e) => {
95
- dispatch('click', { data: tooltip?.data });
96
- }}
97
- />
98
- </Html>
99
-
100
227
  {#if tooltip}
101
228
  <Html>
102
229
  <div
@@ -118,3 +245,69 @@ function hideTooltip(event) {
118
245
  <slot name="highlight" data={tooltip?.data} />
119
246
  </Svg>
120
247
  {/if}
248
+
249
+ {#if mode === 'bisect' || mode === 'quadtree'}
250
+ <Html>
251
+ <div
252
+ class="absolute"
253
+ style="width: {$width}px; height: {$height}px; background: _red; z-index: 9999"
254
+ on:touchstart={handleTooltip}
255
+ on:touchmove={handleTooltip}
256
+ on:mousemove={handleTooltip}
257
+ on:mouseleave={hideTooltip}
258
+ on:click={(e) => {
259
+ dispatch('click', { data: tooltip?.data });
260
+ }}
261
+ />
262
+ </Html>
263
+ {:else if mode === 'voronoi'}
264
+ <Svg>
265
+ {#each points as point, i}
266
+ <g class="tooltip-voronoi">
267
+ <path
268
+ d={voronoi.renderCell(i)}
269
+ style:fill="transparent"
270
+ style:stroke={debug ? 'red' : 'transparent'}
271
+ on:mousemove={(e) => handleTooltip(e, point.data)}
272
+ on:mouseleave={hideTooltip}
273
+ />
274
+ </g>
275
+ {/each}
276
+ </Svg>
277
+ {:else if mode === 'bounds' || mode === 'band'}
278
+ <Svg>
279
+ <g class="tooltip-rects">
280
+ {#each rects as rect}
281
+ <rect
282
+ x={rect.x}
283
+ y={rect.y}
284
+ width={rect.width}
285
+ height={rect.height}
286
+ style:fill="transparent"
287
+ style:stroke={debug ? 'red' : 'transparent'}
288
+ on:mousemove={(e) => handleTooltip(e, rect.data)}
289
+ on:mouseleave={hideTooltip}
290
+ />
291
+ {/each}
292
+ </g>
293
+ </Svg>
294
+ {/if}
295
+
296
+ {#if mode === 'quadtree' && debug}
297
+ <Svg>
298
+ <ChartClipPath>
299
+ <g class="tooltip-quadtree">
300
+ {#each quadtreeRects(quadtree, false) as rect}
301
+ <rect
302
+ x={rect.x}
303
+ y={rect.y}
304
+ width={rect.width}
305
+ height={rect.height}
306
+ stroke="red"
307
+ fill="none"
308
+ />
309
+ {/each}
310
+ </g>
311
+ </ChartClipPath>
312
+ </Svg>
313
+ {/if}
@@ -1,6 +1,7 @@
1
1
  import { SvelteComponentTyped } from "svelte";
2
2
  declare const __propDef: {
3
3
  props: {
4
+ mode?: 'bisect' | 'voronoi' | 'quadtree' | 'bounds' | 'band';
4
5
  snapToDataX?: boolean;
5
6
  snapToDataY?: boolean;
6
7
  findTooltipData?: 'closest' | 'left' | 'right';
@@ -8,6 +9,8 @@ declare const __propDef: {
8
9
  leftOffset?: number;
9
10
  contained?: 'container' | false;
10
11
  animate?: boolean;
12
+ radius?: number;
13
+ debug?: boolean;
11
14
  };
12
15
  events: {
13
16
  click: CustomEvent<{
package/package.json CHANGED
@@ -3,15 +3,17 @@
3
3
  "author": "Sean Lynch <techniq35@gmail.com>",
4
4
  "license": "MIT",
5
5
  "repository": "techniq/layerchart",
6
- "version": "0.6.3",
6
+ "version": "0.6.6",
7
7
  "devDependencies": {
8
8
  "@rollup/plugin-dsv": "^2.0.3",
9
9
  "@sveltejs/adapter-vercel": "^1.0.0-next.58",
10
10
  "@sveltejs/kit": "^1.0.0-next.350",
11
11
  "@tailwindcss/typography": "^0.5.2",
12
12
  "@types/d3-array": "^3.0.3",
13
+ "@types/d3-delaunay": "^6.0.1",
13
14
  "@types/d3-dsv": "^3.0.0",
14
15
  "@types/d3-hierarchy": "^3.1.0",
16
+ "@types/d3-quadtree": "^3.0.2",
15
17
  "@types/d3-sankey": "^0.11.2",
16
18
  "@types/d3-scale": "^4.0.2",
17
19
  "@types/d3-shape": "^3.1.0",
@@ -35,9 +37,11 @@
35
37
  "dependencies": {
36
38
  "@mdi/js": "^6.7.96",
37
39
  "d3-array": "^3.1.6",
40
+ "d3-delaunay": "^6.0.2",
38
41
  "d3-dsv": "^3.0.1",
39
42
  "d3-hierarchy": "^3.1.2",
40
43
  "d3-interpolate-path": "^2.2.3",
44
+ "d3-quadtree": "^3.0.1",
41
45
  "d3-sankey": "^0.12.3",
42
46
  "d3-scale": "^4.0.2",
43
47
  "d3-scale-chromatic": "^3.0.0",
@@ -102,6 +106,7 @@
102
106
  "./utils/math": "./utils/math.js",
103
107
  "./utils/path": "./utils/path.js",
104
108
  "./utils/pivot": "./utils/pivot.js",
109
+ "./utils/quadtree": "./utils/quadtree.js",
105
110
  "./utils/scales": "./utils/scales.js",
106
111
  "./utils/stack": "./utils/stack.js",
107
112
  "./utils/string": "./utils/string.js",
package/utils/event.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { isSVGElement, isSVGGraphicsElement, isSVGSVGElement, isTouchEvent } from 'svelte-ux/types/typeGuards';
2
2
  // See: https://github.com/airbnb/visx/blob/master/packages/visx-event/src/localPointGeneric.ts
3
+ // TODO: Matches event.layerX/Y, but are deprecated (https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/layerX).
4
+ // Similar and could be replaced by event.offsetX/Y (but not identical)
3
5
  export function localPoint(node, event) {
4
6
  if (!node || !event)
5
7
  return null;
@@ -17,6 +17,17 @@ export declare function createDateSeries(options: {
17
17
  }): {
18
18
  date: Date;
19
19
  }[];
20
+ export declare function createTimeSeries(options: {
21
+ count?: number;
22
+ min: number;
23
+ max: number;
24
+ keys: Array<string>;
25
+ value: 'number' | 'integer';
26
+ }): {
27
+ name: string;
28
+ startDate: Date;
29
+ endDate: Date;
30
+ }[];
20
31
  export declare const wideData: {
21
32
  year: string;
22
33
  apples: number;
package/utils/genData.js CHANGED
@@ -1,4 +1,4 @@
1
- import { subDays } from 'date-fns';
1
+ import { addMinutes, startOfDay, startOfToday, subDays } from 'date-fns';
2
2
  import { degreesToRadians, radiansToDegrees } from './math';
3
3
  /**
4
4
  * Get random number between min (inclusive) and max (exclusive)
@@ -17,7 +17,7 @@ export function getRandomInteger(min, max, includeMax = true) {
17
17
  return Math.floor(Math.random() * (max - min + (includeMax ? 1 : 0)) + min);
18
18
  }
19
19
  export function createDateSeries(options) {
20
- const now = new Date();
20
+ const now = startOfToday();
21
21
  const count = options.count ?? 10;
22
22
  const min = options.min;
23
23
  const max = options.max;
@@ -34,6 +34,30 @@ export function createDateSeries(options) {
34
34
  };
35
35
  });
36
36
  }
37
+ export function createTimeSeries(options) {
38
+ const count = options.count ?? 10;
39
+ const min = options.min;
40
+ const max = options.max;
41
+ const keys = options.keys ?? ['value'];
42
+ let lastStartDate = startOfDay(new Date());
43
+ const timeSeries = Array.from({ length: count }).map((_, i) => {
44
+ const startDate = addMinutes(lastStartDate, getRandomInteger(0, 60));
45
+ const endDate = addMinutes(startDate, getRandomInteger(5, 60));
46
+ lastStartDate = startDate;
47
+ return {
48
+ name: `item ${i + 1}`,
49
+ startDate,
50
+ endDate,
51
+ ...Object.fromEntries(keys.map((key) => {
52
+ return [
53
+ key,
54
+ options.value === 'integer' ? getRandomInteger(min, max) : getRandomNumber(min, max)
55
+ ];
56
+ }))
57
+ };
58
+ });
59
+ return timeSeries;
60
+ }
37
61
  export const wideData = [
38
62
  { year: '2019', apples: 3840, bananas: 1920, cherries: 960, dates: 400 },
39
63
  { year: '2018', apples: 1600, bananas: 1440, cherries: 960, dates: 400 },
package/utils/index.d.ts CHANGED
@@ -1 +1,5 @@
1
+ export { graphFromCsv, graphFromHierarchy, graphFromNode } from './graph';
2
+ export { findAncestor } from './hierarchy';
3
+ export { degreesToRadians, radiansToDegrees } from './math';
4
+ export { createStackData, stackOffsetSeparated } from './stack';
1
5
  export { getMajorTicks, getMinorTicks } from './ticks';
package/utils/index.js CHANGED
@@ -1 +1,5 @@
1
+ export { graphFromCsv, graphFromHierarchy, graphFromNode } from './graph';
2
+ export { findAncestor } from './hierarchy';
3
+ export { degreesToRadians, radiansToDegrees } from './math';
4
+ export { createStackData, stackOffsetSeparated } from './stack';
1
5
  export { getMajorTicks, getMinorTicks } from './ticks';
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Transverse guadtree and generate rect dimensions
3
+ */
4
+ export declare function quadtreeRects(quadtree: any, showLeaves?: boolean): any[];
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Transverse guadtree and generate rect dimensions
3
+ */
4
+ export function quadtreeRects(quadtree, showLeaves = true) {
5
+ const rects = [];
6
+ quadtree.visit((node, x0, y0, x1, y1) => {
7
+ if (showLeaves || Array.isArray(node)) {
8
+ rects.push({ x: x0, y: y0, width: x1 - x0, height: y1 - y0 });
9
+ }
10
+ });
11
+ return rects;
12
+ }
package/utils/scales.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { tweened } from 'svelte/motion';
1
+ import { tweened, spring } from 'svelte/motion';
2
2
  import { MotionOptions } from '../stores/motionStore';
3
3
  /**
4
4
  * Implemenation for missing `scaleBand().invert()`
@@ -10,6 +10,11 @@ import { MotionOptions } from '../stores/motionStore';
10
10
  */
11
11
  export declare function scaleBandInvert(scale: any): (value: any) => any;
12
12
  export declare function isScaleBand(scale: any): boolean;
13
+ /**
14
+ * Generic way to invert a scale value, handling scaleBand and continuous scales (linear, time, etc).
15
+ * Useful to map mouse event location (x,y) to domain value
16
+ */
17
+ export declare function scaleInvert(scale: any, value: number): any;
13
18
  /**
14
19
  * Animate d3-scale as domain and/or range are updated using tweened store
15
20
  */
@@ -19,7 +24,15 @@ export declare function tweenedScale(scale: any, tweenedOptions?: Parameters<typ
19
24
  range: (values: any) => Promise<void>;
20
25
  };
21
26
  /**
22
- * Create a store wrapper around a d3-scale which interpolates the domain and/or range using `tweened()` or `spring()` stores. Fallbacks to `writable()` if not interpolating
27
+ * Animate d3-scale as domain and/or range are updated using spring store
28
+ */
29
+ export declare function springScale(scale: any, springOptions?: Parameters<typeof spring>[1]): {
30
+ subscribe: (this: void, run: import("svelte/store").Subscriber<any>, invalidate?: (value?: any) => void) => import("svelte/store").Unsubscriber;
31
+ domain: (values: any) => Promise<void>;
32
+ range: (values: any) => Promise<void>;
33
+ };
34
+ /**
35
+ * Create a store wrapper around a d3-scale which interpolates the domain and/or range using `tweened()` or `spring()` stores. Fallbacks to `writable()` store if not interpolating
23
36
  */
24
37
  export declare function motionScale(scale: any, options: MotionOptions): {
25
38
  subscribe: (this: void, run: import("svelte/store").Subscriber<any>, invalidate?: (value?: any) => void) => import("svelte/store").Unsubscriber;
package/utils/scales.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { derived } from 'svelte/store';
2
- import { tweened } from 'svelte/motion';
2
+ import { tweened, spring } from 'svelte/motion';
3
3
  import { motionStore } from '../stores/motionStore';
4
4
  /**
5
5
  * Implemenation for missing `scaleBand().invert()`
@@ -22,6 +22,18 @@ export function scaleBandInvert(scale) {
22
22
  export function isScaleBand(scale) {
23
23
  return typeof scale.bandwidth === 'function';
24
24
  }
25
+ /**
26
+ * Generic way to invert a scale value, handling scaleBand and continuous scales (linear, time, etc).
27
+ * Useful to map mouse event location (x,y) to domain value
28
+ */
29
+ export function scaleInvert(scale, value) {
30
+ if (isScaleBand(scale)) {
31
+ return scaleBandInvert(scale)(value);
32
+ }
33
+ else {
34
+ return scale.invert(value);
35
+ }
36
+ }
25
37
  /**
26
38
  * Animate d3-scale as domain and/or range are updated using tweened store
27
39
  */
@@ -45,7 +57,29 @@ export function tweenedScale(scale, tweenedOptions = {}) {
45
57
  };
46
58
  }
47
59
  /**
48
- * Create a store wrapper around a d3-scale which interpolates the domain and/or range using `tweened()` or `spring()` stores. Fallbacks to `writable()` if not interpolating
60
+ * Animate d3-scale as domain and/or range are updated using spring store
61
+ */
62
+ export function springScale(scale, springOptions = {}) {
63
+ const domainStore = spring(undefined, springOptions);
64
+ const rangeStore = spring(undefined, springOptions);
65
+ const tweenedScale = derived([domainStore, rangeStore], ([domain, range]) => {
66
+ const scaleInstance = scale.domain ? scale : scale(); // support `scaleLinear` or `scaleLinear()` (which could have `.interpolate()` and others set)
67
+ if (domain) {
68
+ scaleInstance.domain(domain);
69
+ }
70
+ if (range) {
71
+ scaleInstance.range(range);
72
+ }
73
+ return scaleInstance;
74
+ });
75
+ return {
76
+ subscribe: tweenedScale.subscribe,
77
+ domain: (values) => domainStore.set(values),
78
+ range: (values) => rangeStore.set(values)
79
+ };
80
+ }
81
+ /**
82
+ * Create a store wrapper around a d3-scale which interpolates the domain and/or range using `tweened()` or `spring()` stores. Fallbacks to `writable()` store if not interpolating
49
83
  */
50
84
  export function motionScale(scale, options) {
51
85
  const domainStore = motionStore(undefined, options);
package/utils/stack.d.ts CHANGED
@@ -1,9 +1,11 @@
1
- import { stackOffsetNone } from 'd3-shape';
1
+ import { stackOffsetNone, stackOrderNone } from 'd3-shape';
2
+ declare type OrderType = typeof stackOrderNone;
2
3
  declare type OffsetType = typeof stackOffsetNone;
3
4
  export declare function createStackData(data: any[], options: {
4
5
  xKey: string;
5
6
  groupBy?: string;
6
7
  stackBy?: string;
8
+ order?: OrderType;
7
9
  offset?: OffsetType;
8
10
  }): any[];
9
11
  /**
package/utils/stack.js CHANGED
@@ -10,7 +10,7 @@ export function createStackData(data, options) {
10
10
  const itemData = d.slice(-1)[0]; // last item
11
11
  const pivotData = pivotWider(itemData, options.xKey, options.stackBy, 'value');
12
12
  const stackKeys = [...new Set(itemData.map((d) => d[options.stackBy]))];
13
- const stackData = stack().keys(stackKeys).offset(options.offset)(pivotData);
13
+ const stackData = stack().keys(stackKeys).order(options.order).offset(options.offset)(pivotData);
14
14
  //console.log({ pivotData, stackData })
15
15
  return stackData.flatMap((series) => {
16
16
  //console.log({ series })
@@ -29,7 +29,7 @@ export function createStackData(data, options) {
29
29
  // Stack only
30
30
  const pivotData = pivotWider(data, options.xKey, options.stackBy, 'value');
31
31
  const stackKeys = [...new Set(data.map((d) => d[options.stackBy]))];
32
- const stackData = stack().keys(stackKeys).offset(options.offset)(pivotData);
32
+ const stackData = stack().keys(stackKeys).order(options.order).offset(options.offset)(pivotData);
33
33
  const result = stackData.flatMap((series) => {
34
34
  return series.flatMap((s) => {
35
35
  return {