layerchart 0.92.1 → 0.93.1

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.
@@ -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>