layerchart 0.6.4 → 0.6.5
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 +57 -21
- package/components/HighlightRect.svelte +11 -3
- package/components/Text2.svelte +3 -94
- package/components/Text2.svelte.d.ts +7 -13
- package/components/Tooltip.svelte +31 -9
- package/package.json +1 -1
- package/utils/genData.js +2 -2
- package/utils/scales.d.ts +10 -2
- package/utils/scales.js +24 -2
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,4 +1,5 @@
|
|
|
1
|
-
<script>import {
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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)
|
|
56
|
+
y: $yGet(data) + yOffset,
|
|
24
57
|
color: getColor(0)
|
|
25
58
|
}
|
|
26
59
|
];
|
|
60
|
+
}
|
|
27
61
|
</script>
|
|
28
62
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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) - (
|
|
23
|
+
width = ($xScale(nextDataPoint) ?? 0) - (xCoord ?? 0);
|
|
16
24
|
}
|
|
17
25
|
$: dimensions = {
|
|
18
|
-
x:
|
|
19
|
-
y:
|
|
26
|
+
x: xCoord - (isBand ? ($xScale.padding() * $xScale.step()) / 2 : 0),
|
|
27
|
+
y: 0,
|
|
20
28
|
width,
|
|
21
29
|
height: $yRange[0]
|
|
22
30
|
};
|
package/components/Text2.svelte
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script>import
|
|
1
|
+
<script>import '$lib/utils/string';
|
|
2
2
|
/*
|
|
3
3
|
TODO:
|
|
4
4
|
- [ ] Handle styled text (use <slot /> to measure?)
|
|
@@ -12,10 +12,6 @@ Reference:
|
|
|
12
12
|
- https://airbnb.io/visx/text
|
|
13
13
|
- https://github.com/airbnb/visx/blob/master/packages/visx-demo/src/pages/text.tsx
|
|
14
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
15
|
/** x position of the text. */
|
|
20
16
|
export let x = 0;
|
|
21
17
|
/** y position of the text. */
|
|
@@ -24,104 +20,17 @@ export let y = 0;
|
|
|
24
20
|
export let dx = 0;
|
|
25
21
|
/** dy offset of the text. */
|
|
26
22
|
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
23
|
/** Horizontal text anchor. */
|
|
34
24
|
export let textAnchor = 'start';
|
|
35
25
|
/** Vertical text anchor. */
|
|
36
26
|
export let verticalAnchor = 'end'; // default SVG behavior
|
|
37
27
|
/** Rotational angle of the text. */
|
|
38
28
|
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
29
|
</script>
|
|
121
30
|
|
|
122
31
|
<!-- overflow: visible; allow contents to be shown outside element -->
|
|
123
32
|
<!-- paint-order: stroke; support stroke outlining text -->
|
|
124
|
-
<svg x={dx} y={dy} style="overflow: visible; paint-order: stroke;">
|
|
33
|
+
<!-- <svg x={dx} y={dy} style="overflow: visible; paint-order: stroke;">
|
|
125
34
|
{#if isValidXOrY(x) && isValidXOrY(y)}
|
|
126
35
|
<text {x} {y} {transform} text-anchor={textAnchor} {...$$restProps}>
|
|
127
36
|
{#each wordsByLines as line, index}
|
|
@@ -131,4 +40,4 @@ function isValidXOrY(xOrY) {
|
|
|
131
40
|
{/each}
|
|
132
41
|
</text>
|
|
133
42
|
{/if}
|
|
134
|
-
</svg>
|
|
43
|
+
</svg> -->
|
|
@@ -1,19 +1,13 @@
|
|
|
1
1
|
import { SvelteComponentTyped } from "svelte";
|
|
2
2
|
declare const __propDef: {
|
|
3
3
|
props: {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
lineHeight?: string;
|
|
12
|
-
capHeight?: string;
|
|
13
|
-
scaleToFit?: boolean;
|
|
14
|
-
textAnchor?: 'start' | 'middle' | 'end' | 'inherit';
|
|
15
|
-
verticalAnchor?: 'start' | 'middle' | 'end' | 'inherit';
|
|
16
|
-
rotate?: number;
|
|
4
|
+
/** x position of the text. */ x?: string | number;
|
|
5
|
+
/** y position of the text. */ y?: string | number;
|
|
6
|
+
/** dx offset of the text. */ dx?: string | number;
|
|
7
|
+
/** dy offset of the text. */ dy?: string | number;
|
|
8
|
+
/** Horizontal text anchor. */ textAnchor?: 'start' | 'middle' | 'end' | 'inherit';
|
|
9
|
+
/** Vertical text anchor. */ verticalAnchor?: 'start' | 'middle' | 'end' | 'inherit';
|
|
10
|
+
/** Rotational angle of the text. */ rotate?: number;
|
|
17
11
|
};
|
|
18
12
|
events: {
|
|
19
13
|
[evt: string]: CustomEvent<any>;
|
|
@@ -46,21 +46,43 @@ function handleTooltip(event) {
|
|
|
46
46
|
const localY = point?.y ?? 0;
|
|
47
47
|
let tooltipData;
|
|
48
48
|
if (isScaleBand($xScale)) {
|
|
49
|
-
// `x` value at mouse coordinate
|
|
50
|
-
const
|
|
51
|
-
tooltipData = $flatData.find((d) => $x(d) ===
|
|
49
|
+
// `x` value at mouse/touch coordinate
|
|
50
|
+
const valueAtPoint = scaleBandInvert($xScale)(localX);
|
|
51
|
+
tooltipData = $flatData.find((d) => $x(d) === valueAtPoint);
|
|
52
52
|
}
|
|
53
53
|
else {
|
|
54
|
-
// `x` value at mouse coordinate
|
|
55
|
-
const
|
|
56
|
-
const bisectX = bisector(
|
|
57
|
-
|
|
54
|
+
// `x` value at mouse/touch coordinate
|
|
55
|
+
const valueAtPoint = $xScale.invert(localX);
|
|
56
|
+
const bisectX = bisector((d) => {
|
|
57
|
+
const value = $x(d);
|
|
58
|
+
if (Array.isArray(value)) {
|
|
59
|
+
// `x` accessor with multiple properties (ex. `x={['start', 'end']})`)
|
|
60
|
+
// Using first value. Consider using average, max, etc
|
|
61
|
+
// const midpoint = new Date((value[1].valueOf() + value[0].getTime()) / 2);
|
|
62
|
+
// return midpoint;
|
|
63
|
+
return value[0];
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
}).left;
|
|
69
|
+
const index = bisectX($flatData, valueAtPoint, 1);
|
|
58
70
|
const data0 = $flatData[index - 1];
|
|
59
71
|
const data1 = $flatData[index];
|
|
60
72
|
switch (findTooltipData) {
|
|
61
73
|
case 'closest':
|
|
62
|
-
|
|
63
|
-
|
|
74
|
+
if (data1 === undefined) {
|
|
75
|
+
tooltipData = data0;
|
|
76
|
+
}
|
|
77
|
+
else if (data0 === undefined) {
|
|
78
|
+
tooltipData = data1;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
tooltipData =
|
|
82
|
+
Number(valueAtPoint) - Number($x(data0)) > Number($x(data1)) - Number(valueAtPoint)
|
|
83
|
+
? data1
|
|
84
|
+
: data0;
|
|
85
|
+
}
|
|
64
86
|
break;
|
|
65
87
|
case 'left':
|
|
66
88
|
tooltipData = data0;
|
package/package.json
CHANGED
package/utils/genData.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { subDays } from 'date-fns';
|
|
1
|
+
import { 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;
|
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()`
|
|
@@ -19,7 +19,15 @@ export declare function tweenedScale(scale: any, tweenedOptions?: Parameters<typ
|
|
|
19
19
|
range: (values: any) => Promise<void>;
|
|
20
20
|
};
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
22
|
+
* Animate d3-scale as domain and/or range are updated using spring store
|
|
23
|
+
*/
|
|
24
|
+
export declare function springScale(scale: any, springOptions?: Parameters<typeof spring>[1]): {
|
|
25
|
+
subscribe: (this: void, run: import("svelte/store").Subscriber<any>, invalidate?: (value?: any) => void) => import("svelte/store").Unsubscriber;
|
|
26
|
+
domain: (values: any) => Promise<void>;
|
|
27
|
+
range: (values: any) => Promise<void>;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* 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
31
|
*/
|
|
24
32
|
export declare function motionScale(scale: any, options: MotionOptions): {
|
|
25
33
|
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()`
|
|
@@ -45,7 +45,29 @@ export function tweenedScale(scale, tweenedOptions = {}) {
|
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
47
|
/**
|
|
48
|
-
*
|
|
48
|
+
* Animate d3-scale as domain and/or range are updated using spring store
|
|
49
|
+
*/
|
|
50
|
+
export function springScale(scale, springOptions = {}) {
|
|
51
|
+
const domainStore = spring(undefined, springOptions);
|
|
52
|
+
const rangeStore = spring(undefined, springOptions);
|
|
53
|
+
const tweenedScale = derived([domainStore, rangeStore], ([domain, range]) => {
|
|
54
|
+
const scaleInstance = scale.domain ? scale : scale(); // support `scaleLinear` or `scaleLinear()` (which could have `.interpolate()` and others set)
|
|
55
|
+
if (domain) {
|
|
56
|
+
scaleInstance.domain(domain);
|
|
57
|
+
}
|
|
58
|
+
if (range) {
|
|
59
|
+
scaleInstance.range(range);
|
|
60
|
+
}
|
|
61
|
+
return scaleInstance;
|
|
62
|
+
});
|
|
63
|
+
return {
|
|
64
|
+
subscribe: tweenedScale.subscribe,
|
|
65
|
+
domain: (values) => domainStore.set(values),
|
|
66
|
+
range: (values) => rangeStore.set(values)
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 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
71
|
*/
|
|
50
72
|
export function motionScale(scale, options) {
|
|
51
73
|
const domainStore = motionStore(undefined, options);
|