layerchart 0.6.4 → 0.6.7
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.
- package/components/AxisY.svelte +1 -4
- package/components/HighlightLine.svelte +96 -22
- package/components/HighlightLine.svelte.d.ts +1 -0
- package/components/HighlightRect.svelte +56 -17
- package/components/HighlightRect.svelte.d.ts +1 -0
- package/components/Points.svelte +22 -6
- package/components/Points.svelte.d.ts +5 -0
- package/components/Tooltip.svelte +235 -42
- package/components/Tooltip.svelte.d.ts +3 -0
- package/package.json +6 -2
- package/utils/event.js +2 -0
- package/utils/genData.d.ts +11 -0
- package/utils/genData.js +26 -2
- package/utils/quadtree.d.ts +4 -0
- package/utils/quadtree.js +12 -0
- package/utils/scales.d.ts +15 -2
- package/utils/scales.js +36 -2
- package/components/Text2.svelte +0 -134
- package/components/Text2.svelte.d.ts +0 -28
package/components/AxisY.svelte
CHANGED
|
@@ -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,42 +1,116 @@
|
|
|
1
1
|
<script>import { getContext } from 'svelte';
|
|
2
2
|
import { get } from 'svelte/store';
|
|
3
|
+
import { max } from 'd3-array';
|
|
4
|
+
import { isScaleBand } from '../utils/scales';
|
|
3
5
|
import Circle from './Circle.svelte';
|
|
4
6
|
import Line from './Line.svelte';
|
|
7
|
+
const { xScale, xRange, xGet, yScale, yRange, yGet, zScale } = getContext('LayerCake');
|
|
5
8
|
export let data;
|
|
6
9
|
export let color = undefined;
|
|
7
|
-
|
|
10
|
+
export let axis = 'x';
|
|
8
11
|
// TODO: Fix circle points being backwards for stack (see AreaStack)
|
|
9
12
|
$: x = $xGet(data);
|
|
13
|
+
$: xOffset = isScaleBand($xScale) ? $xScale.bandwidth() / 2 : 0;
|
|
14
|
+
$: y = $yGet(data);
|
|
15
|
+
$: yOffset = isScaleBand($yScale) ? $yScale.bandwidth() / 2 : 0;
|
|
10
16
|
function getColor(index) {
|
|
11
17
|
return color ?? get(zScale)(index) ?? 'var(--color-blue-500)';
|
|
12
18
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
let lines = [];
|
|
20
|
+
$: {
|
|
21
|
+
lines = [];
|
|
22
|
+
if (axis === 'x' || axis === 'both') {
|
|
23
|
+
if (Array.isArray(x)) {
|
|
24
|
+
// `x` accessor with multiple properties (ex. `x={['start', 'end']})`)
|
|
25
|
+
lines = [
|
|
26
|
+
...lines,
|
|
27
|
+
...x.map((xItem, i) => ({
|
|
28
|
+
x1: xItem + xOffset,
|
|
29
|
+
y1: 0,
|
|
30
|
+
x2: xItem + xOffset,
|
|
31
|
+
y2: max($yRange)
|
|
32
|
+
}))
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
lines = [
|
|
37
|
+
...lines,
|
|
38
|
+
{
|
|
39
|
+
x1: x + xOffset,
|
|
40
|
+
y1: 0,
|
|
41
|
+
x2: x + xOffset,
|
|
42
|
+
y2: max($yRange)
|
|
43
|
+
}
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (axis === 'y' || axis === 'both') {
|
|
48
|
+
if (Array.isArray(y)) {
|
|
49
|
+
// `y` accessor with multiple properties (ex. `y={['start', 'end']})`)
|
|
50
|
+
lines = [
|
|
51
|
+
...lines,
|
|
52
|
+
...y.map((yItem, i) => ({
|
|
53
|
+
x1: 0,
|
|
54
|
+
y1: yItem + yOffset,
|
|
55
|
+
x2: max($xRange),
|
|
56
|
+
y2: yItem + yOffset
|
|
57
|
+
}))
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
lines = [
|
|
62
|
+
...lines,
|
|
63
|
+
{
|
|
64
|
+
x1: 0,
|
|
65
|
+
y1: y + yOffset,
|
|
66
|
+
x2: max($xRange),
|
|
67
|
+
y2: y + yOffset
|
|
68
|
+
}
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
let points = [];
|
|
74
|
+
$: if (Array.isArray(x)) {
|
|
75
|
+
// `x` accessor with multiple properties (ex. `x={['start', 'end']})`)
|
|
76
|
+
points = x.map((xItem, i) => ({
|
|
77
|
+
x: xItem + xOffset,
|
|
78
|
+
y: $yGet(data) + yOffset,
|
|
79
|
+
color: getColor(i)
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
else if (Array.isArray(data)) {
|
|
83
|
+
// Stack series
|
|
84
|
+
points = data.map((yValue, i) => ({
|
|
85
|
+
x: x + xOffset,
|
|
86
|
+
y: $yScale(yValue) + yOffset,
|
|
87
|
+
color: getColor(i)
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
points = [
|
|
21
92
|
{
|
|
22
|
-
x,
|
|
23
|
-
y: $yGet(data)
|
|
93
|
+
x: x + xOffset,
|
|
94
|
+
y: $yGet(data) + yOffset,
|
|
24
95
|
color: getColor(0)
|
|
25
96
|
}
|
|
26
97
|
];
|
|
98
|
+
}
|
|
27
99
|
</script>
|
|
28
100
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
101
|
+
{#each lines as line}
|
|
102
|
+
<Line
|
|
103
|
+
spring
|
|
104
|
+
x1={line.x1}
|
|
105
|
+
y1={line.y1}
|
|
106
|
+
x2={line.x2}
|
|
107
|
+
y2={line.y2}
|
|
108
|
+
stroke="rgba(0,0,0,.5)"
|
|
109
|
+
stroke-width={2}
|
|
110
|
+
style="pointerEvents: none"
|
|
111
|
+
stroke-dasharray="2,2"
|
|
112
|
+
/>
|
|
113
|
+
{/each}
|
|
40
114
|
|
|
41
115
|
{#each points as point}
|
|
42
116
|
<Circle
|
|
@@ -1,25 +1,64 @@
|
|
|
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';
|
|
5
|
+
const { flatData, x, xScale, xDomain, xRange, xGet, yScale, yDomain, yRange, yGet } = getContext('LayerCake');
|
|
4
6
|
export let data;
|
|
5
|
-
|
|
6
|
-
$:
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
let index = $flatData.findIndex((d) => Number($x(d)) === Number($x(data)));
|
|
14
|
-
let nextDataPoint = $x($flatData[index + 1]);
|
|
15
|
-
width = ($xScale(nextDataPoint) ?? 0) - ($xGet(data) ?? 0);
|
|
16
|
-
}
|
|
17
|
-
$: dimensions = {
|
|
18
|
-
x: $xGet(data) - (isBand ? ($xScale.padding() * $xScale.step()) / 2 : 0),
|
|
19
|
-
y: -$padding.top,
|
|
20
|
-
width,
|
|
21
|
-
height: $yRange[0]
|
|
7
|
+
export let axis = isScaleBand($yScale) ? 'y' : 'x';
|
|
8
|
+
$: xCoord = $xGet(data);
|
|
9
|
+
$: yCoord = $yGet(data);
|
|
10
|
+
let dimensions = {
|
|
11
|
+
x: 0,
|
|
12
|
+
y: 0,
|
|
13
|
+
width: 0,
|
|
14
|
+
height: 0
|
|
22
15
|
};
|
|
16
|
+
$: {
|
|
17
|
+
if (axis === 'x' || axis === 'both') {
|
|
18
|
+
if (isScaleBand($xScale)) {
|
|
19
|
+
dimensions.width = $xScale.step();
|
|
20
|
+
}
|
|
21
|
+
else if (Array.isArray(xCoord)) {
|
|
22
|
+
// `x` accessor with multiple properties (ex. `x={['start', 'end']})`)
|
|
23
|
+
// Use first/last values for width
|
|
24
|
+
dimensions.width = max(xCoord) - min(xCoord);
|
|
25
|
+
xCoord = min(xCoord); // Use left-most value for top left of rect
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
// Find width to next data point
|
|
29
|
+
const index = $flatData.findIndex((d) => Number($x(d)) === Number($x(data)));
|
|
30
|
+
const isLastPoint = index + 1 === $flatData.length;
|
|
31
|
+
const nextDataPoint = isLastPoint ? max($xDomain) : $x($flatData[index + 1]);
|
|
32
|
+
dimensions.width = ($xScale(nextDataPoint) ?? 0) - (xCoord ?? 0);
|
|
33
|
+
}
|
|
34
|
+
dimensions.x = xCoord - (isScaleBand($xScale) ? ($xScale.padding() * $xScale.step()) / 2 : 0);
|
|
35
|
+
if (axis === 'x') {
|
|
36
|
+
dimensions.height = $yRange[0];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (axis === 'y' || axis === 'both') {
|
|
40
|
+
if (isScaleBand($yScale)) {
|
|
41
|
+
dimensions.height = $yScale.step();
|
|
42
|
+
}
|
|
43
|
+
else if (Array.isArray(xCoord)) {
|
|
44
|
+
// `y` accessor with multiple properties (ex. `y={['start', 'end']})`)
|
|
45
|
+
// Use first/last values for width
|
|
46
|
+
dimensions.height = max(yCoord) - min(yCoord);
|
|
47
|
+
yCoord = min(yCoord); // Use left-most value for top left of rect
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
// Find width to next data point
|
|
51
|
+
const index = $flatData.findIndex((d) => Number($x(d)) === Number($x(data)));
|
|
52
|
+
const isLastPoint = index + 1 === $flatData.length;
|
|
53
|
+
const nextDataPoint = isLastPoint ? max($yDomain) : $x($flatData[index + 1]);
|
|
54
|
+
dimensions.height = ($yScale(nextDataPoint) ?? 0) - (yCoord ?? 0);
|
|
55
|
+
}
|
|
56
|
+
dimensions.y = yCoord - (isScaleBand($yScale) ? ($yScale.padding() * $yScale.step()) / 2 : 0);
|
|
57
|
+
if (axis === 'y') {
|
|
58
|
+
dimensions.width = $xRange[1];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
23
62
|
</script>
|
|
24
63
|
|
|
25
64
|
{#if Number.isFinite(dimensions.x)}
|
package/components/Points.svelte
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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) ? min(xValue) : xValue;
|
|
141
|
+
const y = Array.isArray(yValue) ? min(yValue) : 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 min(value);
|
|
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 min(value);
|
|
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.
|
|
6
|
+
"version": "0.6.7",
|
|
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",
|
|
@@ -80,7 +84,6 @@
|
|
|
80
84
|
"./components/RectClipPath.svelte": "./components/RectClipPath.svelte",
|
|
81
85
|
"./components/Sankey.svelte": "./components/Sankey.svelte",
|
|
82
86
|
"./components/Text.svelte": "./components/Text.svelte",
|
|
83
|
-
"./components/Text2.svelte": "./components/Text2.svelte",
|
|
84
87
|
"./components/Threshold.svelte": "./components/Threshold.svelte",
|
|
85
88
|
"./components/Tooltip.svelte": "./components/Tooltip.svelte",
|
|
86
89
|
"./components/Tree.svelte": "./components/Tree.svelte",
|
|
@@ -103,6 +106,7 @@
|
|
|
103
106
|
"./utils/math": "./utils/math.js",
|
|
104
107
|
"./utils/path": "./utils/path.js",
|
|
105
108
|
"./utils/pivot": "./utils/pivot.js",
|
|
109
|
+
"./utils/quadtree": "./utils/quadtree.js",
|
|
106
110
|
"./utils/scales": "./utils/scales.js",
|
|
107
111
|
"./utils/stack": "./utils/stack.js",
|
|
108
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;
|
package/utils/genData.d.ts
CHANGED
|
@@ -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 =
|
|
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 },
|
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
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/components/Text2.svelte
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
<script>import { getStringWidth } from '../utils/string';
|
|
2
|
-
/*
|
|
3
|
-
TODO:
|
|
4
|
-
- [ ] Handle styled text (use <slot /> to measure?)
|
|
5
|
-
- [ ] Simplify by using `alignment-baseline` / `dominant-baseline`, rework multiline or drop support, etc
|
|
6
|
-
- https://svelte.dev/repl/f12d3003313a43ba8a0be53e5786f1c7?version=3.44.3
|
|
7
|
-
- https://observablehq.com/@neocartocnrs/cheat-sheet-on-texts-in-svg
|
|
8
|
-
|
|
9
|
-
Reference:
|
|
10
|
-
- https://bl.ocks.org/mbostock/7555321
|
|
11
|
-
- https://github.com/airbnb/visx/blob/master/packages/visx-text/src/Text.tsx
|
|
12
|
-
- https://airbnb.io/visx/text
|
|
13
|
-
- https://github.com/airbnb/visx/blob/master/packages/visx-demo/src/pages/text.tsx
|
|
14
|
-
*/
|
|
15
|
-
/** text value */
|
|
16
|
-
export let value = 0;
|
|
17
|
-
/** Maximum width to occupy (approximate as words are not split). */
|
|
18
|
-
export let width = undefined;
|
|
19
|
-
/** x position of the text. */
|
|
20
|
-
export let x = 0;
|
|
21
|
-
/** y position of the text. */
|
|
22
|
-
export let y = 0;
|
|
23
|
-
/** dx offset of the text. */
|
|
24
|
-
export let dx = 0;
|
|
25
|
-
/** dy offset of the text. */
|
|
26
|
-
export let dy = 0;
|
|
27
|
-
/** Desired "line height" of the text, implemented as y offsets. */
|
|
28
|
-
export let lineHeight = '1em';
|
|
29
|
-
/** Cap height of the text. */
|
|
30
|
-
export let capHeight = '0.71em'; // Magic number from d3
|
|
31
|
-
/** Whether to scale the fontSize to accommodate the specified width. */
|
|
32
|
-
export let scaleToFit = false;
|
|
33
|
-
/** Horizontal text anchor. */
|
|
34
|
-
export let textAnchor = 'start';
|
|
35
|
-
/** Vertical text anchor. */
|
|
36
|
-
export let verticalAnchor = 'end'; // default SVG behavior
|
|
37
|
-
/** Rotational angle of the text. */
|
|
38
|
-
export let rotate = undefined;
|
|
39
|
-
let wordsByLines = [];
|
|
40
|
-
let wordsWithWidth = [];
|
|
41
|
-
let spaceWidth = 0;
|
|
42
|
-
let style = undefined; // TODO: read from DOM?
|
|
43
|
-
$: words = value ? value.toString().split(/(?:(?!\u00A0+)\s+)/) : [];
|
|
44
|
-
$: wordsWithWidth = words.map((word) => ({
|
|
45
|
-
word,
|
|
46
|
-
width: getStringWidth(word, style) || 0
|
|
47
|
-
}));
|
|
48
|
-
$: spaceWidth = getStringWidth('\u00A0', style) || 0;
|
|
49
|
-
$: wordsByLines = wordsWithWidth.reduce((result, item) => {
|
|
50
|
-
const currentLine = result[result.length - 1];
|
|
51
|
-
if (currentLine &&
|
|
52
|
-
(width == null || scaleToFit || (currentLine.width || 0) + item.width + spaceWidth < width)) {
|
|
53
|
-
// Word can be added to an existing line
|
|
54
|
-
currentLine.words.push(item.word);
|
|
55
|
-
currentLine.width = currentLine.width || 0;
|
|
56
|
-
currentLine.width += item.width + spaceWidth;
|
|
57
|
-
}
|
|
58
|
-
else {
|
|
59
|
-
// Add first word to line or word is too long to scaleToFit on existing line
|
|
60
|
-
const newLine = { words: [item.word], width: item.width };
|
|
61
|
-
result.push(newLine);
|
|
62
|
-
}
|
|
63
|
-
return result;
|
|
64
|
-
}, []);
|
|
65
|
-
$: lines = wordsByLines.length;
|
|
66
|
-
/**
|
|
67
|
-
* Convert css value to pixel value (ex. 0.71em => 11.36)
|
|
68
|
-
*/
|
|
69
|
-
function getPixelValue(cssValue) {
|
|
70
|
-
// TODO: Properly measure pixel values using DOM (handle inherited font size, zoom, etc)
|
|
71
|
-
// console.log(cssValue);
|
|
72
|
-
const [match, value, units] = cssValue.match(/([\d.]+)(\D+)/);
|
|
73
|
-
// console.log({ value, units });
|
|
74
|
-
const number = Number(value);
|
|
75
|
-
switch (units) {
|
|
76
|
-
case 'px':
|
|
77
|
-
return number;
|
|
78
|
-
case 'em':
|
|
79
|
-
case 'rem':
|
|
80
|
-
return number * 16;
|
|
81
|
-
default:
|
|
82
|
-
return 0;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
let startDy = 0;
|
|
86
|
-
$: if (verticalAnchor === 'start') {
|
|
87
|
-
startDy = getPixelValue(capHeight);
|
|
88
|
-
}
|
|
89
|
-
else if (verticalAnchor === 'middle') {
|
|
90
|
-
startDy = ((lines - 1) / 2) * -getPixelValue(lineHeight) + getPixelValue(capHeight) / 2;
|
|
91
|
-
}
|
|
92
|
-
else {
|
|
93
|
-
startDy = (lines - 1) * -getPixelValue(lineHeight);
|
|
94
|
-
}
|
|
95
|
-
let scaleTransform = '';
|
|
96
|
-
$: if (scaleToFit &&
|
|
97
|
-
lines > 0 &&
|
|
98
|
-
typeof x == 'number' &&
|
|
99
|
-
typeof y == 'number' &&
|
|
100
|
-
typeof width == 'number') {
|
|
101
|
-
const lineWidth = wordsByLines[0].width || 1;
|
|
102
|
-
const sx = width / lineWidth;
|
|
103
|
-
const sy = sx;
|
|
104
|
-
const originX = x - sx * x;
|
|
105
|
-
const originY = y - sy * y;
|
|
106
|
-
scaleTransform = `matrix(${sx}, 0, 0, ${sy}, ${originX}, ${originY})`;
|
|
107
|
-
}
|
|
108
|
-
else {
|
|
109
|
-
scaleTransform = '';
|
|
110
|
-
}
|
|
111
|
-
$: rotateTransform = rotate ? `rotate(${rotate}, ${x}, ${y})` : '';
|
|
112
|
-
$: transform = `${scaleTransform} ${rotateTransform}`;
|
|
113
|
-
function isValidXOrY(xOrY) {
|
|
114
|
-
return (
|
|
115
|
-
// number that is not NaN or Infinity
|
|
116
|
-
(typeof xOrY === 'number' && Number.isFinite(xOrY)) ||
|
|
117
|
-
// for percentage
|
|
118
|
-
typeof xOrY === 'string');
|
|
119
|
-
}
|
|
120
|
-
</script>
|
|
121
|
-
|
|
122
|
-
<!-- overflow: visible; allow contents to be shown outside element -->
|
|
123
|
-
<!-- paint-order: stroke; support stroke outlining text -->
|
|
124
|
-
<svg x={dx} y={dy} style="overflow: visible; paint-order: stroke;">
|
|
125
|
-
{#if isValidXOrY(x) && isValidXOrY(y)}
|
|
126
|
-
<text {x} {y} {transform} text-anchor={textAnchor} {...$$restProps}>
|
|
127
|
-
{#each wordsByLines as line, index}
|
|
128
|
-
<tspan {x} dy={index === 0 ? startDy : lineHeight}>
|
|
129
|
-
{line.words.join(' ')}
|
|
130
|
-
</tspan>
|
|
131
|
-
{/each}
|
|
132
|
-
</text>
|
|
133
|
-
{/if}
|
|
134
|
-
</svg>
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { SvelteComponentTyped } from "svelte";
|
|
2
|
-
declare const __propDef: {
|
|
3
|
-
props: {
|
|
4
|
-
[x: string]: any;
|
|
5
|
-
value?: string | number;
|
|
6
|
-
width?: number;
|
|
7
|
-
x?: string | number;
|
|
8
|
-
y?: string | number;
|
|
9
|
-
dx?: string | number;
|
|
10
|
-
dy?: string | number;
|
|
11
|
-
lineHeight?: string;
|
|
12
|
-
capHeight?: string;
|
|
13
|
-
scaleToFit?: boolean;
|
|
14
|
-
textAnchor?: 'start' | 'middle' | 'end' | 'inherit';
|
|
15
|
-
verticalAnchor?: 'start' | 'middle' | 'end' | 'inherit';
|
|
16
|
-
rotate?: number;
|
|
17
|
-
};
|
|
18
|
-
events: {
|
|
19
|
-
[evt: string]: CustomEvent<any>;
|
|
20
|
-
};
|
|
21
|
-
slots: {};
|
|
22
|
-
};
|
|
23
|
-
export declare type Text2Props = typeof __propDef.props;
|
|
24
|
-
export declare type Text2Events = typeof __propDef.events;
|
|
25
|
-
export declare type Text2Slots = typeof __propDef.slots;
|
|
26
|
-
export default class Text2 extends SvelteComponentTyped<Text2Props, Text2Events, Text2Slots> {
|
|
27
|
-
}
|
|
28
|
-
export {};
|