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.
@@ -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
  };
@@ -1,4 +1,4 @@
1
- <script>import { getStringWidth } from '../utils/string';
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
- [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;
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 xValue = scaleBandInvert($xScale)(localX);
51
- tooltipData = $flatData.find((d) => $x(d) === xValue);
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 xValue = $xScale.invert(localX);
56
- const bisectX = bisector($x).left;
57
- const index = bisectX($flatData, xValue, 1);
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
- tooltipData =
63
- Number(xValue) - Number($x(data0)) > Number($x(data1)) - Number(xValue) ? data1 : data0;
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
@@ -3,7 +3,7 @@
3
3
  "author": "Sean Lynch <techniq35@gmail.com>",
4
4
  "license": "MIT",
5
5
  "repository": "techniq/layerchart",
6
- "version": "0.6.4",
6
+ "version": "0.6.5",
7
7
  "devDependencies": {
8
8
  "@rollup/plugin-dsv": "^2.0.3",
9
9
  "@sveltejs/adapter-vercel": "^1.0.0-next.58",
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 = 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;
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
- * 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
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
- * 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
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);