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.
- package/dist/components/BrushContext.svelte +476 -0
- package/dist/components/BrushContext.svelte.d.ts +73 -0
- package/dist/components/Chart.svelte +64 -47
- package/dist/components/Chart.svelte.d.ts +51 -12
- package/dist/components/Circle.svelte +3 -0
- package/dist/components/Highlight.svelte +22 -20
- package/dist/components/charts/AreaChart.svelte +39 -30
- package/dist/components/charts/LineChart.svelte +35 -29
- package/dist/components/charts/PieChart.svelte.d.ts +45 -9
- package/dist/components/charts/ScatterChart.svelte +21 -24
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.js +1 -1
- package/dist/components/tooltip/TooltipContext.svelte +7 -7
- package/dist/components/tooltip/TooltipContext.svelte.d.ts +2 -2
- package/dist/utils/math.d.ts +2 -0
- package/dist/utils/math.js +9 -0
- package/package.json +1 -1
- package/dist/components/Brush.svelte +0 -436
|
@@ -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>
|