layerchart 0.92.1 → 0.93.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -115,8 +115,8 @@
115
115
 
116
116
  /** Exposed to allow binding in Chart */
117
117
  export let tooltip = writable({
118
- y: 0,
119
118
  x: 0,
119
+ y: 0,
120
120
  data: null as any,
121
121
  show: showTooltip,
122
122
  hide: hideTooltip,
@@ -393,12 +393,12 @@
393
393
  <!-- svelte-ignore a11y-click-events-have-key-events -->
394
394
  <!-- svelte-ignore a11y-no-static-element-interactions -->
395
395
  <div
396
- style:width="{$width}px"
397
- style:height="{$height}px"
398
396
  style:top="{$padding.top}px"
399
397
  style:left="{$padding.left}px"
398
+ style:width="{$width}px"
399
+ style:height="{$height}px"
400
400
  class={cls(
401
- 'tooltip-trigger absolute touch-none',
401
+ 'TooltipContext absolute touch-none',
402
402
  debug && triggerPointerEvents && 'bg-danger/10 outline outline-danger'
403
403
  )}
404
404
  on:pointerenter={triggerPointerEvents ? showTooltip : undefined}
@@ -410,13 +410,13 @@
410
410
  }
411
411
  }}
412
412
  >
413
- <!-- Rendering slot within tooltip-trigger allows pointer events to bubble up (ex. Brush) -->
413
+ <!-- Rendering slot within TooltipContext to allow pointer events to bubble up (ex. Brush) -->
414
414
  <div
415
415
  class="absolute"
416
- style:width="{$containerWidth}px"
417
- style:height="{$containerHeight}px"
418
416
  style:top="-{$padding.top ?? 0}px"
419
417
  style:left="-{$padding.left ?? 0}px"
418
+ style:width="{$containerWidth}px"
419
+ style:height="{$containerHeight}px"
420
420
  >
421
421
  <slot tooltip={$tooltip} />
422
422
 
@@ -30,8 +30,8 @@ declare const __propDef: {
30
30
  data: any;
31
31
  }) => any;
32
32
  /** Exposed to allow binding in Chart */ tooltip?: import("svelte/store").Writable<{
33
- y: number;
34
33
  x: number;
34
+ y: number;
35
35
  data: any;
36
36
  show: (e: PointerEvent, tooltipData?: any) => void;
37
37
  hide: () => void;
@@ -44,8 +44,8 @@ declare const __propDef: {
44
44
  slots: {
45
45
  default: {
46
46
  tooltip: {
47
- y: number;
48
47
  x: number;
48
+ y: number;
49
49
  data: any;
50
50
  show: (e: PointerEvent, tooltipData?: any) => void;
51
51
  hide: () => void;
@@ -29,3 +29,5 @@ export declare function celsiusToFahrenheit(temperature: number): number;
29
29
  export declare function fahrenheitToCelsius(temperature: number): number;
30
30
  /** Parse percent string (`50%`) to decimal (`0.5`) */
31
31
  export declare function parsePercent(percent: string | number): number;
32
+ /** Add second value while maintaining `Date` or `number` type */
33
+ export declare function add(value1: Date | number, value2: number): number | Date;
@@ -55,3 +55,12 @@ export function parsePercent(percent) {
55
55
  return Number(percent.replace('%', '')) / 100;
56
56
  }
57
57
  }
58
+ /** Add second value while maintaining `Date` or `number` type */
59
+ export function add(value1, value2) {
60
+ if (value1 instanceof Date) {
61
+ return new Date(value1.getTime() + value2);
62
+ }
63
+ else {
64
+ return value1 + value2;
65
+ }
66
+ }
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "author": "Sean Lynch <techniq35@gmail.com>",
5
5
  "license": "MIT",
6
6
  "repository": "techniq/layerchart",
7
- "version": "0.92.1",
7
+ "version": "0.93.0",
8
8
  "devDependencies": {
9
9
  "@changesets/cli": "^2.27.12",
10
10
  "@mdi/js": "^7.4.47",
@@ -1,436 +0,0 @@
1
- <script lang="ts">
2
- import { type ComponentProps } from 'svelte';
3
- import { extent, min, max } from 'd3-array';
4
- import { clamp } from '@layerstack/utils';
5
- import { cls } from '@layerstack/tailwind';
6
- import { format as formatValue, type FormatType, Logger } from '@layerstack/utils';
7
-
8
- import { chartContext } from './ChartContext.svelte';
9
- import Frame from './Frame.svelte';
10
- import Group from './Group.svelte';
11
- import Text from './Text.svelte';
12
-
13
- import { localPoint } from '../utils/event.js';
14
- import type { DomainType } from '../utils/scales.js';
15
- import { asAny } from '../utils/types.js';
16
- import Rect from './Rect.svelte';
17
-
18
- const { xScale, yScale, width, height, padding, config } = chartContext();
19
-
20
- /** Axis to apply brushing */
21
- export let axis: 'x' | 'y' | 'both' = 'x';
22
-
23
- /** Size of draggable handles (width/height) */
24
- export let handleSize = 5;
25
-
26
- /** Only show range while actively brushing. Useful with `brushEnd` event */
27
- export let resetOnEnd = false;
28
-
29
- export let xDomain: DomainType = $xScale.domain() as [number, number];
30
- export let yDomain: DomainType = $yScale.domain() as [number, number];
31
-
32
- export let labels: ComponentProps<Text> | boolean = false;
33
-
34
- /** Mode of operation
35
- * `integrated`: use with single chart
36
- * `separated`: use with separate (typically smaller) chart and state can be managed externally (sync with other charts, etc). Show active selection when domain does not equal original
37
- */
38
- export let mode: 'integrated' | 'separated' = 'integrated';
39
-
40
- // Capture original domains for reset()
41
- const originalXDomain = $config.xDomain;
42
- const originalYDomain = $config.yDomain;
43
-
44
- $: [xDomainMin, xDomainMax] = extent<number>($xScale.domain()) as [number, number];
45
- $: [yDomainMin, yDomainMax] = extent<number>($yScale.domain()) as [number, number];
46
-
47
- /** Attributes passed to range <rect> element */
48
- export let range: Partial<ComponentProps<Rect>> | undefined = undefined;
49
-
50
- /** Attributes passed to handle <rect> elements */
51
- export let handle: Partial<ComponentProps<Rect>> | undefined = undefined;
52
-
53
- /** Apply format to labels, if shown */
54
- export let format: FormatType | undefined = undefined;
55
-
56
- export let classes: {
57
- root?: string;
58
- frame?: string;
59
- range?: string;
60
- handle?: string;
61
- labels?: string;
62
- } = {};
63
-
64
- export let onchange: (detail: { xDomain?: DomainType; yDomain?: DomainType }) => void = () => {};
65
- export let onbrushstart: (detail: {
66
- xDomain?: DomainType;
67
- yDomain?: DomainType;
68
- }) => void = () => {};
69
- export let onbrushend: (detail: {
70
- xDomain?: DomainType;
71
- yDomain?: DomainType;
72
- }) => void = () => {};
73
- export let onreset: (detail: { xDomain?: DomainType; yDomain?: DomainType }) => void = () => {};
74
-
75
- let frameEl: SVGRectElement;
76
-
77
- const logger = new Logger('Brush');
78
- const RESET_THRESHOLD = 1; // size of pointer delta to ignore
79
-
80
- function handler(
81
- fn: (
82
- start: {
83
- xDomain: [number, number];
84
- yDomain: [number, number];
85
- value: { x: number; y: number };
86
- },
87
- value: { x: number; y: number }
88
- ) => void
89
- ) {
90
- return (e: PointerEvent) => {
91
- const startPoint = localPoint(frameEl, e);
92
- const start = {
93
- xDomain: [xDomain?.[0] ?? xDomainMin, xDomain?.[1] ?? xDomainMax] as [number, number],
94
- yDomain: [yDomain?.[0] ?? yDomainMin, yDomain?.[1] ?? yDomainMax] as [number, number],
95
- value: {
96
- x: $xScale.invert?.((startPoint?.x ?? 0) - $padding.left),
97
- y: $yScale.invert?.((startPoint?.y ?? 0) - $padding.top),
98
- },
99
- };
100
-
101
- onbrushstart({ xDomain, yDomain });
102
-
103
- const onPointerMove = (e: PointerEvent) => {
104
- const currentPoint = localPoint(frameEl, e);
105
- fn(start, {
106
- x: $xScale.invert?.((currentPoint?.x ?? 0) - $padding.left),
107
- y: $yScale.invert?.((currentPoint?.y ?? 0) - $padding.top),
108
- });
109
-
110
- onchange({ xDomain, yDomain });
111
- };
112
-
113
- const onPointerUp = (e: PointerEvent) => {
114
- const currentPoint = localPoint(frameEl, e);
115
- const xPointDelta = Math.abs((startPoint?.x ?? 0) - (currentPoint?.x ?? 0));
116
- const yPointDelta = Math.abs((startPoint?.y ?? 0) - (currentPoint?.y ?? 0));
117
-
118
- if (
119
- (e.target == frameEl && xPointDelta < RESET_THRESHOLD && yPointDelta < RESET_THRESHOLD) ||
120
- rangeWidth < RESET_THRESHOLD ||
121
- rangeHeight < RESET_THRESHOLD
122
- ) {
123
- // Clicked on frame, or pointer delta was <1
124
- logger.info('resetting due to frame click');
125
- reset();
126
- onchange({ xDomain, yDomain });
127
- } else {
128
- logger.info('drag', {
129
- target: e.target,
130
- xPointDelta,
131
- yPointDelta,
132
- rangeWidth,
133
- rangeHeight,
134
- });
135
- }
136
-
137
- onbrushend({ xDomain, yDomain });
138
-
139
- if (resetOnEnd) {
140
- reset();
141
- }
142
-
143
- window.removeEventListener('pointermove', onPointerMove);
144
- window.removeEventListener('pointerup', onPointerUp);
145
- };
146
-
147
- window.addEventListener('pointermove', onPointerMove);
148
- window.addEventListener('pointerup', onPointerUp);
149
- };
150
- }
151
-
152
- /** Add second value while maintaining `Date` or `number` type */
153
- function add(value1: Date | number, value2: number) {
154
- if (value1 instanceof Date) {
155
- return new Date(value1.getTime() + value2);
156
- } else {
157
- return value1 + value2;
158
- }
159
- }
160
-
161
- const createRange = handler((start, value) => {
162
- isActive = true;
163
-
164
- xDomain = [
165
- // @ts-expect-error
166
- clamp(min([start.value.x, value.x]), xDomainMin, xDomainMax),
167
- // @ts-expect-error
168
- clamp(max([start.value.x, value.x]), xDomainMin, xDomainMax),
169
- ];
170
- // xDomain = [start.value.x, value.x];
171
-
172
- yDomain = [
173
- // @ts-expect-error
174
- clamp(min([start.value.y, value.y]), yDomainMin, yDomainMax),
175
- // @ts-expect-error
176
- clamp(max([start.value.y, value.y]), yDomainMin, yDomainMax),
177
- ];
178
- });
179
-
180
- const adjustRange = handler((start, value) => {
181
- const dx = clamp(
182
- value.x - start.value.x,
183
- xDomainMin - start.xDomain[0],
184
- xDomainMax - start.xDomain[1]
185
- );
186
- xDomain = [add(start.xDomain[0], dx), add(start.xDomain[1], dx)];
187
-
188
- const dy = clamp(
189
- value.y - start.value.y,
190
- yDomainMin - start.yDomain[0],
191
- yDomainMax - start.yDomain[1]
192
- );
193
- yDomain = [add(start.yDomain[0], dy), add(start.yDomain[1], dy)];
194
- });
195
-
196
- const adjustBottom = handler((start, value) => {
197
- yDomain = [
198
- clamp(value.y > start.yDomain[1] ? start.yDomain[1] : value.y, yDomainMin, yDomainMax),
199
- clamp(value.y > start.yDomain[1] ? value.y : start.yDomain[1], yDomainMin, yDomainMax),
200
- ];
201
- });
202
-
203
- const adjustTop = handler((start, value) => {
204
- yDomain = [
205
- clamp(value.y < start.yDomain[1] ? value.y : start.yDomain[0], yDomainMin, yDomainMax),
206
- clamp(value.y < start.yDomain[1] ? start.yDomain[0] : value.y, yDomainMin, yDomainMax),
207
- ];
208
- });
209
-
210
- const adjustLeft = handler((start, value) => {
211
- xDomain = [
212
- clamp(value.x > start.xDomain[1] ? start.xDomain[1] : value.x, xDomainMin, xDomainMax),
213
- clamp(value.x > start.xDomain[1] ? value.x : start.xDomain[1], xDomainMin, xDomainMax),
214
- ];
215
- });
216
-
217
- const adjustRight = handler((start, value) => {
218
- xDomain = [
219
- clamp(value.x < start.xDomain[0] ? value.x : start.xDomain[0], xDomainMin, xDomainMax),
220
- clamp(value.x < start.xDomain[0] ? start.xDomain[0] : value.x, xDomainMin, xDomainMax),
221
- ];
222
- });
223
-
224
- function reset() {
225
- isActive = false;
226
-
227
- xDomain = originalXDomain;
228
- yDomain = originalYDomain;
229
-
230
- onreset({ xDomain, yDomain });
231
- }
232
-
233
- function selectAll() {
234
- xDomain = [xDomainMin, xDomainMax];
235
- yDomain = [yDomainMin, yDomainMax];
236
- }
237
-
238
- $: top = $yScale(yDomain?.[1]);
239
- $: bottom = $yScale(yDomain?.[0]);
240
- $: left = $xScale(xDomain?.[0]);
241
- $: right = $xScale(xDomain?.[1]);
242
-
243
- $: rangeTop = axis === 'both' || axis === 'y' ? top : 0;
244
- $: rangeLeft = axis === 'both' || axis === 'x' ? left : 0;
245
- $: rangeWidth = axis === 'both' || axis === 'x' ? right - left : $width;
246
- $: rangeHeight = axis === 'both' || axis === 'y' ? bottom - top : $height;
247
-
248
- let isActive = false;
249
- $: if (mode === 'separated') {
250
- // Set reactively to handle cases where xDomain/yDomain are set externally (ex. `bind:xDomain`)
251
- isActive =
252
- xDomain?.[0]?.valueOf() !== originalXDomain?.[0]?.valueOf() ||
253
- xDomain?.[1]?.valueOf() !== originalXDomain?.[1]?.valueOf() ||
254
- yDomain?.[0]?.valueOf() !== originalYDomain?.[0]?.valueOf() ||
255
- yDomain?.[1]?.valueOf() !== originalYDomain?.[1]?.valueOf();
256
- }
257
- </script>
258
-
259
- <g class={cls('Brush select-none', classes.root, $$props.class)}>
260
- <Frame
261
- class={cls('frame', 'fill-transparent', classes.frame)}
262
- onpointerdown={createRange}
263
- ondblclick={() => selectAll()}
264
- bind:element={frameEl}
265
- />
266
-
267
- {#if isActive}
268
- <Group x={rangeLeft} y={rangeTop} class="range">
269
- <slot name="range" {rangeWidth} {rangeHeight}>
270
- <Rect
271
- width={rangeWidth}
272
- height={rangeHeight}
273
- class={cls(
274
- 'cursor-move select-none',
275
- range?.fill == null && 'fill-surface-content/10',
276
- classes.range
277
- )}
278
- onpointerdown={adjustRange}
279
- ondblclick={() => reset()}
280
- {...range}
281
- />
282
- </slot>
283
- </Group>
284
-
285
- {#if axis === 'both' || axis === 'y'}
286
- <Group
287
- x={rangeLeft}
288
- y={rangeTop}
289
- class="handle top"
290
- onpointerdown={adjustTop}
291
- ondblclick={() => {
292
- if (yDomain) {
293
- yDomain[0] = yDomainMin;
294
- onchange({ xDomain, yDomain });
295
- }
296
- }}
297
- >
298
- <slot name="handle" edge="top" {rangeWidth} {rangeHeight}>
299
- <Rect
300
- width={rangeWidth}
301
- height={handleSize}
302
- class={cls('fill-transparent cursor-ns-resize select-none', classes.handle)}
303
- {...handle}
304
- />
305
- </slot>
306
- </Group>
307
-
308
- <Group
309
- x={rangeLeft}
310
- y={bottom - handleSize + 1}
311
- class="handle bottom"
312
- onpointerdown={adjustBottom}
313
- ondblclick={() => {
314
- if (yDomain) {
315
- yDomain[1] = yDomainMax;
316
- }
317
- }}
318
- >
319
- <slot name="handle" edge="bottom" {rangeWidth} {rangeHeight}>
320
- <Rect
321
- width={rangeWidth}
322
- height={handleSize}
323
- class={cls('fill-transparent cursor-ns-resize select-none', classes.handle)}
324
- {...handle}
325
- />
326
- </slot>
327
- </Group>
328
- {/if}
329
-
330
- {#if axis === 'both' || axis === 'x'}
331
- <Group
332
- x={rangeLeft}
333
- y={rangeTop}
334
- class="handle left"
335
- onpointerdown={adjustLeft}
336
- ondblclick={() => {
337
- if (xDomain) {
338
- xDomain[0] = xDomainMin;
339
- onchange({ xDomain, yDomain });
340
- }
341
- }}
342
- >
343
- <slot name="handle" edge="left" {rangeWidth} {rangeHeight}>
344
- <rect
345
- width={handleSize}
346
- height={rangeHeight}
347
- class={cls('fill-transparent cursor-ew-resize select-none', classes.handle)}
348
- {...handle}
349
- />
350
- </slot>
351
- </Group>
352
-
353
- <Group
354
- x={right - handleSize + 1}
355
- y={rangeTop}
356
- class="handle right"
357
- onpointerdown={adjustRight}
358
- ondblclick={() => {
359
- if (xDomain) {
360
- xDomain[1] = xDomainMax;
361
- onchange({ xDomain, yDomain });
362
- }
363
- }}
364
- >
365
- <slot name="handle" edge="right" {rangeWidth} {rangeHeight}>
366
- <Rect
367
- width={handleSize}
368
- height={rangeHeight}
369
- class={cls('fill-transparent cursor-ew-resize select-none', classes.handle)}
370
- {...handle}
371
- />
372
- </slot>
373
- </Group>
374
- {/if}
375
-
376
- <slot name="labels">
377
- {#if labels}
378
- {@const labelClass = cls(
379
- 'text-xs',
380
- classes.labels,
381
- typeof labels === 'object' ? labels.class : null
382
- )}
383
-
384
- {#if axis === 'x' || axis === 'both'}
385
- <Text
386
- x={left}
387
- y={rangeTop + rangeHeight / 2}
388
- dx={-4}
389
- textAnchor="end"
390
- verticalAnchor="middle"
391
- value={formatValue(asAny(xDomain?.[0]), format)}
392
- {...typeof labels === 'object' ? labels : null}
393
- class={labelClass}
394
- />
395
-
396
- <Text
397
- x={right}
398
- y={rangeTop + rangeHeight / 2}
399
- dx={4}
400
- textAnchor="start"
401
- verticalAnchor="middle"
402
- value={formatValue(asAny(xDomain?.[1]), format)}
403
- {...typeof labels === 'object' ? labels : null}
404
- class={labelClass}
405
- />
406
- {/if}
407
-
408
- {#if axis === 'y' || axis === 'both'}
409
- <Text
410
- x={rangeLeft + rangeWidth / 2}
411
- y={top}
412
- dy={-4}
413
- textAnchor="middle"
414
- verticalAnchor="end"
415
- value={formatValue(asAny(yDomain?.[1]), format)}
416
- {...typeof labels === 'object' ? labels : null}
417
- class={labelClass}
418
- />
419
-
420
- <Text
421
- x={rangeLeft + rangeWidth / 2}
422
- y={bottom}
423
- dy={4}
424
- textAnchor="middle"
425
- verticalAnchor="start"
426
- value={formatValue(asAny(yDomain?.[0]), format)}
427
- {...typeof labels === 'object' ? labels : null}
428
- class={labelClass}
429
- />
430
- {/if}
431
- {/if}
432
- </slot>
433
-
434
- <!-- TODO: Add diagonal/corner handles -->
435
- {/if}
436
- </g>