@fjader/svelte 0.2.0 → 0.2.2
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/charts/ChartCell.svelte +191 -34
- package/dist/components/charts/ChartContainer.svelte +3 -0
- package/dist/components/marks/RectYMark.svelte +0 -1
- package/dist/components/rendering/Areas.svelte +2 -3
- package/dist/components/rendering/Lines.svelte +2 -3
- package/dist/components/rendering/ParallelLines.svelte +2 -1
- package/dist/data/DataAccessor.js +1 -1
- package/dist/gpu/ColorScaleShaderLib.d.ts +1 -1
- package/dist/gpu/ColorScaleShaderLib.js +3 -2
- package/dist/gpu/NanShaderLib.d.ts +30 -0
- package/dist/gpu/NanShaderLib.js +42 -0
- package/dist/gpu/ResolveShader.d.ts +1 -1
- package/dist/gpu/ResolveShader.js +4 -3
- package/dist/gpu/SizeScaleShaderLib.d.ts +1 -1
- package/dist/gpu/SizeScaleShaderLib.js +2 -1
- package/dist/gpu/decimation/M4Shader.d.ts +13 -7
- package/dist/gpu/decimation/M4Shader.js +79 -64
- package/dist/interactors/BrushOverlay.svelte +17 -10
- package/dist/interactors/BrushOverlay.svelte.d.ts +2 -2
- package/dist/interactors/DerivedInterval1D.svelte +28 -17
- package/dist/interactors/Interval1D.svelte +52 -46
- package/dist/interactors/Interval2D.svelte +48 -53
- package/dist/interactors/ParallelBrushes.svelte +52 -52
- package/dist/interactors/brush-utils.d.ts +21 -1
- package/dist/interactors/brush-utils.js +36 -8
- package/dist/query-engine/index.d.ts +13 -3
- package/dist/query-engine/index.js +16 -7
- package/dist/spec/index.js +2 -4
- package/dist/types.d.ts +23 -1
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { getContext, onMount } from 'svelte';
|
|
2
|
+
import { getContext, onMount, untrack } from 'svelte';
|
|
3
3
|
import { isBetween } from '@uwdata/mosaic-sql';
|
|
4
4
|
import { BrushState1D } from './brush-state.svelte';
|
|
5
5
|
import BrushOverlay from './BrushOverlay.svelte';
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
isDragging,
|
|
10
10
|
scaleBrushExtent1D,
|
|
11
11
|
createPanModeHandlers,
|
|
12
|
+
brushHandleAt,
|
|
12
13
|
WHEEL_SENSITIVITY
|
|
13
14
|
} from './brush-utils';
|
|
14
15
|
import type { ChartContext, FieldRef, Point, D3Scale } from '../types';
|
|
@@ -103,7 +104,7 @@
|
|
|
103
104
|
let current = 0;
|
|
104
105
|
let isCapturing = false;
|
|
105
106
|
let hasDragged = false;
|
|
106
|
-
let isPanModeActive =
|
|
107
|
+
let isPanModeActive = false;
|
|
107
108
|
|
|
108
109
|
// Reusable point object to reduce GC pressure
|
|
109
110
|
const point = { x: 0, y: 0 };
|
|
@@ -113,9 +114,12 @@
|
|
|
113
114
|
let lastEmittedMax: number | null = null;
|
|
114
115
|
|
|
115
116
|
// Keyboard handlers for Space key pan mode
|
|
116
|
-
const { handleKeyDown, handleKeyUp } = createPanModeHandlers(
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
const { handleKeyDown, handleKeyUp, handleBlur } = createPanModeHandlers(
|
|
118
|
+
(active) => {
|
|
119
|
+
isPanModeActive = active;
|
|
120
|
+
},
|
|
121
|
+
() => chart.reroutePress()
|
|
122
|
+
);
|
|
119
123
|
|
|
120
124
|
// Scale validation
|
|
121
125
|
// Always validate as 'linear': Fjader time scales use float offsets, not Dates,
|
|
@@ -128,19 +132,26 @@
|
|
|
128
132
|
});
|
|
129
133
|
|
|
130
134
|
// Watch for external selection changes (from detail chart pan/zoom)
|
|
131
|
-
// to keep brush visual in sync and update Mosaic filter
|
|
135
|
+
// to keep brush visual in sync and update Mosaic filter.
|
|
136
|
+
// Tracks only the selection extent and its source. The rest runs untracked:
|
|
137
|
+
// emitting the clause reads state it then writes (the filter group's
|
|
138
|
+
// selection, the scales' active values), which made this effect re-run
|
|
139
|
+
// itself without end while a detail chart was panned.
|
|
132
140
|
$effect(() => {
|
|
133
141
|
if (!(scaleState instanceof LinearScaleState)) return;
|
|
134
142
|
const extent = scaleState.selectionExtent;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
143
|
+
const source = scaleState.selectionSource;
|
|
144
|
+
untrack(() => {
|
|
145
|
+
if (extent && source === 'external') {
|
|
146
|
+
brush.setExtent(extent[0], extent[1]);
|
|
147
|
+
lastEmittedMin = extent[0];
|
|
148
|
+
lastEmittedMax = extent[1];
|
|
149
|
+
emitFilterClause(extent[0], extent[1]);
|
|
150
|
+
} else if (!extent && brush.active) {
|
|
151
|
+
// Selection was cleared externally (e.g. detail chart reset) — clear brush
|
|
152
|
+
clearBrush();
|
|
153
|
+
}
|
|
154
|
+
});
|
|
144
155
|
});
|
|
145
156
|
|
|
146
157
|
// Derived screen coordinates — read chart.xScale/yScale directly so the
|
|
@@ -223,16 +234,25 @@
|
|
|
223
234
|
|
|
224
235
|
// --- Pointer Event Handlers (registered as chart context callbacks) ---
|
|
225
236
|
|
|
226
|
-
|
|
237
|
+
/** A press: move or resize on the brush's handles, else draw; decline in pan mode. */
|
|
238
|
+
function handlePointerDown(target: Element, pointerId: number, innerPoint: Point) {
|
|
227
239
|
if (brush.phase !== 'idle') return false;
|
|
228
240
|
if (isPanModeActive) return false;
|
|
229
241
|
|
|
230
|
-
|
|
242
|
+
target.setPointerCapture(pointerId);
|
|
231
243
|
isCapturing = true;
|
|
232
244
|
hasDragged = false;
|
|
233
245
|
|
|
234
246
|
anchor = innerCoord(innerPoint);
|
|
235
247
|
current = anchor;
|
|
248
|
+
|
|
249
|
+
const hit = brushHandleAt(target);
|
|
250
|
+
if (hit?.handle === 'move') {
|
|
251
|
+
brush.phase = 'moving';
|
|
252
|
+
} else if (hit) {
|
|
253
|
+
brush.phase = 'resizing';
|
|
254
|
+
brush.resizeEdge = hit.handle === edgeStart ? 'start' : 'end';
|
|
255
|
+
}
|
|
236
256
|
return true;
|
|
237
257
|
}
|
|
238
258
|
|
|
@@ -280,44 +300,30 @@
|
|
|
280
300
|
(event.target as Element).releasePointerCapture(event.pointerId);
|
|
281
301
|
isCapturing = false;
|
|
282
302
|
|
|
303
|
+
// A press that never moved is a click, which clears the brush.
|
|
283
304
|
if (!hasDragged && brush.active && (brush.phase === 'idle' || brush.phase === 'moving')) {
|
|
284
305
|
clearBrush();
|
|
285
306
|
return true;
|
|
286
307
|
}
|
|
287
308
|
|
|
309
|
+
endDrag();
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Keep what the drag did and go idle. */
|
|
314
|
+
function endDrag() {
|
|
288
315
|
if (brush.phase !== 'idle') {
|
|
289
316
|
emitUpdate();
|
|
290
317
|
brush.phase = 'idle';
|
|
291
318
|
}
|
|
292
|
-
|
|
293
319
|
hasDragged = false;
|
|
294
|
-
return true;
|
|
295
320
|
}
|
|
296
321
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
isCapturing =
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
point.x = event.offsetX;
|
|
304
|
-
point.y = event.offsetY;
|
|
305
|
-
const innerCoords = chart.toInnerCoordinates(point);
|
|
306
|
-
anchor = innerCoord(innerCoords);
|
|
307
|
-
brush.phase = 'moving';
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function handleResizeStart(edge: string, event: PointerEvent) {
|
|
311
|
-
event.stopPropagation();
|
|
312
|
-
(event.target as Element).setPointerCapture(event.pointerId);
|
|
313
|
-
isCapturing = true;
|
|
314
|
-
|
|
315
|
-
point.x = event.offsetX;
|
|
316
|
-
point.y = event.offsetY;
|
|
317
|
-
const innerCoords = chart.toInnerCoordinates(point);
|
|
318
|
-
anchor = innerCoord(innerCoords);
|
|
319
|
-
brush.phase = 'resizing';
|
|
320
|
-
brush.resizeEdge = edge === edgeStart ? 'start' : 'end';
|
|
322
|
+
/** The chart re-routes the press: end the drag as it stands, never as a click. */
|
|
323
|
+
function handleDragEnd() {
|
|
324
|
+
if (!isCapturing) return;
|
|
325
|
+
isCapturing = false;
|
|
326
|
+
endDrag();
|
|
321
327
|
}
|
|
322
328
|
|
|
323
329
|
// Register pointer callbacks; the keys are on <svelte:window> below.
|
|
@@ -325,19 +331,21 @@
|
|
|
325
331
|
chart.onInteractorPointerDown = handlePointerDown;
|
|
326
332
|
chart.onInteractorPointerMove = handlePointerMove;
|
|
327
333
|
chart.onInteractorPointerUp = handlePointerUp;
|
|
334
|
+
chart.onInteractorDragEnd = handleDragEnd;
|
|
328
335
|
chart.activators.add(brushHandle.activate);
|
|
329
336
|
|
|
330
337
|
return () => {
|
|
331
338
|
chart.onInteractorPointerDown = null;
|
|
332
339
|
chart.onInteractorPointerMove = null;
|
|
333
340
|
chart.onInteractorPointerUp = null;
|
|
341
|
+
chart.onInteractorDragEnd = null;
|
|
334
342
|
chart.activators.delete(brushHandle.activate);
|
|
335
343
|
brushHandle.destroy();
|
|
336
344
|
};
|
|
337
345
|
});
|
|
338
346
|
</script>
|
|
339
347
|
|
|
340
|
-
<svelte:window onkeydown={handleKeyDown} onkeyup={handleKeyUp} />
|
|
348
|
+
<svelte:window onkeydown={handleKeyDown} onkeyup={handleKeyUp} onblur={handleBlur} />
|
|
341
349
|
|
|
342
350
|
{#if brush.active && brushScreen}
|
|
343
351
|
<BrushOverlay
|
|
@@ -346,8 +354,6 @@
|
|
|
346
354
|
width={isX ? brushScreen.size : chart.innerWidth}
|
|
347
355
|
height={isX ? chart.innerHeight : brushScreen.size}
|
|
348
356
|
orientation={axis}
|
|
349
|
-
onMoveStart={handleMoveStart}
|
|
350
|
-
onResizeStart={handleResizeStart}
|
|
351
357
|
onWheel={handleWheel}
|
|
352
358
|
/>
|
|
353
359
|
{/if}
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
closeTo,
|
|
9
9
|
isDragging,
|
|
10
10
|
scaleBrushExtent2D,
|
|
11
|
+
createPanModeHandlers,
|
|
12
|
+
brushHandleAt,
|
|
11
13
|
WHEEL_SENSITIVITY
|
|
12
14
|
} from './brush-utils';
|
|
13
15
|
import type { ChartContext, FieldRef, Point } from '../types';
|
|
@@ -104,6 +106,15 @@
|
|
|
104
106
|
let currentY = 0;
|
|
105
107
|
let isCapturing = false;
|
|
106
108
|
let hasDragged = false;
|
|
109
|
+
let isPanModeActive = false;
|
|
110
|
+
|
|
111
|
+
// Space held: presses pan the chart instead of brushing
|
|
112
|
+
const { handleKeyDown, handleKeyUp, handleBlur } = createPanModeHandlers(
|
|
113
|
+
(active) => {
|
|
114
|
+
isPanModeActive = active;
|
|
115
|
+
},
|
|
116
|
+
() => chart.reroutePress()
|
|
117
|
+
);
|
|
107
118
|
|
|
108
119
|
// Reusable point object to reduce GC pressure
|
|
109
120
|
const point = { x: 0, y: 0 };
|
|
@@ -218,10 +229,12 @@
|
|
|
218
229
|
|
|
219
230
|
// --- Pointer Event Handlers (registered as chart context callbacks) ---
|
|
220
231
|
|
|
221
|
-
|
|
232
|
+
/** A press: move or resize on the brush's handles, else draw; decline in pan mode. */
|
|
233
|
+
function handlePointerDown(target: Element, pointerId: number, innerPoint: Point) {
|
|
222
234
|
if (brush.phase !== 'idle') return false;
|
|
235
|
+
if (isPanModeActive) return false;
|
|
223
236
|
|
|
224
|
-
|
|
237
|
+
target.setPointerCapture(pointerId);
|
|
225
238
|
isCapturing = true;
|
|
226
239
|
hasDragged = false;
|
|
227
240
|
|
|
@@ -229,6 +242,23 @@
|
|
|
229
242
|
anchorY = innerPoint.y;
|
|
230
243
|
currentX = innerPoint.x;
|
|
231
244
|
currentY = innerPoint.y;
|
|
245
|
+
|
|
246
|
+
const hit = brushHandleAt(target);
|
|
247
|
+
if (hit?.handle === 'move') {
|
|
248
|
+
brush.phase = 'moving';
|
|
249
|
+
} else if (hit) {
|
|
250
|
+
brush.phase = 'resizing';
|
|
251
|
+
const edge = hit.handle;
|
|
252
|
+
if (edge === 'nw' || edge === 'ne' || edge === 'sw' || edge === 'se') {
|
|
253
|
+
brush.resizeCorner = edge;
|
|
254
|
+
brush.x.resizeEdge = null;
|
|
255
|
+
brush.y.resizeEdge = null;
|
|
256
|
+
} else {
|
|
257
|
+
brush.resizeCorner = null;
|
|
258
|
+
brush.x.resizeEdge = edge === 'left' || edge === 'right' ? edge : null;
|
|
259
|
+
brush.y.resizeEdge = edge === 'top' || edge === 'bottom' ? edge : null;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
232
262
|
return true;
|
|
233
263
|
}
|
|
234
264
|
|
|
@@ -302,11 +332,18 @@
|
|
|
302
332
|
(event.target as Element).releasePointerCapture(event.pointerId);
|
|
303
333
|
isCapturing = false;
|
|
304
334
|
|
|
335
|
+
// A press that never moved is a click, which clears the brush.
|
|
305
336
|
if (!hasDragged && brush.active && (brush.phase === 'idle' || brush.phase === 'moving')) {
|
|
306
337
|
clearBrush();
|
|
307
338
|
return true;
|
|
308
339
|
}
|
|
309
340
|
|
|
341
|
+
endDrag();
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Keep what the drag did and go idle. */
|
|
346
|
+
function endDrag() {
|
|
310
347
|
if (brush.phase !== 'idle') {
|
|
311
348
|
emitClause();
|
|
312
349
|
brush.phase = 'idle';
|
|
@@ -314,58 +351,14 @@
|
|
|
314
351
|
brush.x.resizeEdge = null;
|
|
315
352
|
brush.y.resizeEdge = null;
|
|
316
353
|
}
|
|
317
|
-
|
|
318
354
|
hasDragged = false;
|
|
319
|
-
return true;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function handleMoveStart(event: PointerEvent) {
|
|
323
|
-
event.stopPropagation();
|
|
324
|
-
(event.target as Element).setPointerCapture(event.pointerId);
|
|
325
|
-
isCapturing = true;
|
|
326
|
-
hasDragged = false;
|
|
327
|
-
|
|
328
|
-
point.x = event.offsetX;
|
|
329
|
-
point.y = event.offsetY;
|
|
330
|
-
const innerCoords = chart.toInnerCoordinates(point);
|
|
331
|
-
anchorX = innerCoords.x;
|
|
332
|
-
anchorY = innerCoords.y;
|
|
333
|
-
currentX = innerCoords.x;
|
|
334
|
-
currentY = innerCoords.y;
|
|
335
|
-
brush.phase = 'moving';
|
|
336
355
|
}
|
|
337
356
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
(event.target as HTMLElement).setPointerCapture(event.pointerId);
|
|
344
|
-
isCapturing = true;
|
|
345
|
-
|
|
346
|
-
point.x = event.offsetX;
|
|
347
|
-
point.y = event.offsetY;
|
|
348
|
-
const innerCoords = chart.toInnerCoordinates(point);
|
|
349
|
-
anchorX = innerCoords.x;
|
|
350
|
-
anchorY = innerCoords.y;
|
|
351
|
-
currentX = innerCoords.x;
|
|
352
|
-
currentY = innerCoords.y;
|
|
353
|
-
brush.phase = 'resizing';
|
|
354
|
-
|
|
355
|
-
if (edge === 'nw' || edge === 'ne' || edge === 'sw' || edge === 'se') {
|
|
356
|
-
brush.resizeCorner = edge;
|
|
357
|
-
brush.x.resizeEdge = null;
|
|
358
|
-
brush.y.resizeEdge = null;
|
|
359
|
-
} else {
|
|
360
|
-
brush.resizeCorner = null;
|
|
361
|
-
if (edge === 'left' || edge === 'right') {
|
|
362
|
-
brush.x.resizeEdge = edge;
|
|
363
|
-
brush.y.resizeEdge = null;
|
|
364
|
-
} else {
|
|
365
|
-
brush.x.resizeEdge = null;
|
|
366
|
-
brush.y.resizeEdge = edge;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
357
|
+
/** The chart re-routes the press: end the drag as it stands, never as a click. */
|
|
358
|
+
function handleDragEnd() {
|
|
359
|
+
if (!isCapturing) return;
|
|
360
|
+
isCapturing = false;
|
|
361
|
+
endDrag();
|
|
369
362
|
}
|
|
370
363
|
|
|
371
364
|
// Register callbacks
|
|
@@ -373,18 +366,22 @@
|
|
|
373
366
|
chart.onInteractorPointerDown = handlePointerDown;
|
|
374
367
|
chart.onInteractorPointerMove = handlePointerMove;
|
|
375
368
|
chart.onInteractorPointerUp = handlePointerUp;
|
|
369
|
+
chart.onInteractorDragEnd = handleDragEnd;
|
|
376
370
|
chart.activators.add(brushHandle.activate);
|
|
377
371
|
|
|
378
372
|
return () => {
|
|
379
373
|
chart.onInteractorPointerDown = null;
|
|
380
374
|
chart.onInteractorPointerMove = null;
|
|
381
375
|
chart.onInteractorPointerUp = null;
|
|
376
|
+
chart.onInteractorDragEnd = null;
|
|
382
377
|
chart.activators.delete(brushHandle.activate);
|
|
383
378
|
brushHandle.destroy();
|
|
384
379
|
};
|
|
385
380
|
});
|
|
386
381
|
</script>
|
|
387
382
|
|
|
383
|
+
<svelte:window onkeydown={handleKeyDown} onkeyup={handleKeyUp} onblur={handleBlur} />
|
|
384
|
+
|
|
388
385
|
{#if brush.active && brushScreen}
|
|
389
386
|
<BrushOverlay
|
|
390
387
|
x={brushScreen.x}
|
|
@@ -392,8 +389,6 @@
|
|
|
392
389
|
width={brushScreen.width}
|
|
393
390
|
height={brushScreen.height}
|
|
394
391
|
orientation="2d"
|
|
395
|
-
onMoveStart={handleMoveStart}
|
|
396
|
-
onResizeStart={handleResizeStart}
|
|
397
392
|
onWheel={handleWheel}
|
|
398
393
|
/>
|
|
399
394
|
{/if}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { SvelteMap } from 'svelte/reactivity';
|
|
5
5
|
import BrushOverlay from './BrushOverlay.svelte';
|
|
6
6
|
import ParallelAxisSource, { type ParallelAxis } from './ParallelAxisSource.svelte';
|
|
7
|
-
import { closeTo, isDragging, createPanModeHandlers } from './brush-utils';
|
|
7
|
+
import { closeTo, isDragging, createPanModeHandlers, brushHandleAt } from './brush-utils';
|
|
8
8
|
import { LinearScaleState } from '../scales/LinearScaleState.svelte';
|
|
9
9
|
import type { ChartContext, Point } from '../types';
|
|
10
10
|
import type { FilterGroupRegistry } from '../query-engine/FilterGroupRegistry.svelte';
|
|
@@ -62,10 +62,13 @@
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
// Pan mode (Space key) — when active, pointer events fall through to ChartCell for pan
|
|
65
|
-
let isPanModeActive =
|
|
66
|
-
const { handleKeyDown, handleKeyUp } = createPanModeHandlers(
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
let isPanModeActive = false;
|
|
66
|
+
const { handleKeyDown, handleKeyUp, handleBlur } = createPanModeHandlers(
|
|
67
|
+
(active) => {
|
|
68
|
+
isPanModeActive = active;
|
|
69
|
+
},
|
|
70
|
+
() => chart.reroutePress()
|
|
71
|
+
);
|
|
69
72
|
|
|
70
73
|
// The axis the user is dragging on, by column
|
|
71
74
|
let activeAxis: string | null = null;
|
|
@@ -166,19 +169,35 @@
|
|
|
166
169
|
|
|
167
170
|
// --- Pointer handlers ---
|
|
168
171
|
|
|
169
|
-
|
|
172
|
+
/**
|
|
173
|
+
* A press: move or resize on an axis's brush handles (that axis, from the
|
|
174
|
+
* overlay's owner), else draw on the nearest axis; decline in pan mode.
|
|
175
|
+
*/
|
|
176
|
+
function handlePointerDown(target: Element, pointerId: number, innerPoint: Point) {
|
|
170
177
|
if (!ps) return false;
|
|
171
178
|
if (isPanModeActive) return false; // Fall through to ChartCell for parallel axis pan
|
|
172
179
|
|
|
173
|
-
const
|
|
180
|
+
const hit = brushHandleAt(target);
|
|
181
|
+
const col = hit?.owner ?? nearestAxis(innerPoint.x);
|
|
174
182
|
if (col == null || !axes.has(col)) return false;
|
|
183
|
+
// A drag that starts on a brush overlay hovers its axis, even without a
|
|
184
|
+
// pointer move over the axis first.
|
|
185
|
+
if (hit) hoverAxis(col);
|
|
175
186
|
|
|
176
|
-
|
|
187
|
+
target.setPointerCapture(pointerId);
|
|
177
188
|
activeAxis = col;
|
|
178
189
|
isCapturing = true;
|
|
179
190
|
hasDragged = false;
|
|
180
191
|
anchor = innerPoint.y;
|
|
181
192
|
current = anchor;
|
|
193
|
+
|
|
194
|
+
const brush = axes.get(col)!.brush;
|
|
195
|
+
if (hit?.handle === 'move') {
|
|
196
|
+
brush.phase = 'moving';
|
|
197
|
+
} else if (hit) {
|
|
198
|
+
brush.phase = 'resizing';
|
|
199
|
+
brush.resizeEdge = hit.handle === 'top' ? 'start' : 'end';
|
|
200
|
+
}
|
|
182
201
|
return true;
|
|
183
202
|
}
|
|
184
203
|
|
|
@@ -240,59 +259,39 @@
|
|
|
240
259
|
const col = activeAxis;
|
|
241
260
|
activeAxis = null;
|
|
242
261
|
const brush = axes.get(col)?.brush;
|
|
243
|
-
if (!brush) {
|
|
244
|
-
hasDragged = false;
|
|
245
|
-
return true;
|
|
246
|
-
}
|
|
247
262
|
|
|
248
|
-
|
|
263
|
+
// A press that never moved is a click, which clears the brush.
|
|
264
|
+
if (
|
|
265
|
+
brush &&
|
|
266
|
+
!hasDragged &&
|
|
267
|
+
brush.active &&
|
|
268
|
+
(brush.phase === 'idle' || brush.phase === 'moving')
|
|
269
|
+
) {
|
|
249
270
|
clearBrush(col);
|
|
250
271
|
return true;
|
|
251
272
|
}
|
|
252
273
|
|
|
253
|
-
|
|
254
|
-
emitUpdate(col);
|
|
255
|
-
brush.phase = 'idle';
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
hasDragged = false;
|
|
274
|
+
endDrag(col);
|
|
259
275
|
return true;
|
|
260
276
|
}
|
|
261
277
|
|
|
262
|
-
|
|
278
|
+
/** Keep what the drag on an axis did and go idle. */
|
|
279
|
+
function endDrag(col: string) {
|
|
263
280
|
const brush = axes.get(col)?.brush;
|
|
264
|
-
if (
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
hoverAxis(col);
|
|
269
|
-
(event.target as Element).setPointerCapture(event.pointerId);
|
|
270
|
-
activeAxis = col;
|
|
271
|
-
isCapturing = true;
|
|
281
|
+
if (brush && brush.phase !== 'idle') {
|
|
282
|
+
emitUpdate(col);
|
|
283
|
+
brush.phase = 'idle';
|
|
284
|
+
}
|
|
272
285
|
hasDragged = false;
|
|
273
|
-
|
|
274
|
-
point.x = event.offsetX;
|
|
275
|
-
point.y = event.offsetY;
|
|
276
|
-
const innerCoords = chart.toInnerCoordinates(point);
|
|
277
|
-
anchor = innerCoords.y;
|
|
278
|
-
brush.phase = 'moving';
|
|
279
286
|
}
|
|
280
287
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
if (!
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
isCapturing = true;
|
|
289
|
-
|
|
290
|
-
point.x = event.offsetX;
|
|
291
|
-
point.y = event.offsetY;
|
|
292
|
-
const innerCoords = chart.toInnerCoordinates(point);
|
|
293
|
-
anchor = innerCoords.y;
|
|
294
|
-
brush.phase = 'resizing';
|
|
295
|
-
brush.resizeEdge = edge === 'top' ? 'start' : 'end';
|
|
288
|
+
/** The chart re-routes the press: end the drag as it stands, never as a click. */
|
|
289
|
+
function handleDragEnd() {
|
|
290
|
+
if (!isCapturing || activeAxis == null) return;
|
|
291
|
+
isCapturing = false;
|
|
292
|
+
const col = activeAxis;
|
|
293
|
+
activeAxis = null;
|
|
294
|
+
endDrag(col);
|
|
296
295
|
}
|
|
297
296
|
|
|
298
297
|
// Brush screen extents, one per active brush, at its axis's x-position —
|
|
@@ -324,18 +323,20 @@
|
|
|
324
323
|
chart.onInteractorPointerDown = handlePointerDown;
|
|
325
324
|
chart.onInteractorPointerMove = handlePointerMove;
|
|
326
325
|
chart.onInteractorPointerUp = handlePointerUp;
|
|
326
|
+
chart.onInteractorDragEnd = handleDragEnd;
|
|
327
327
|
chart.activators.add(activateHoveredAxis);
|
|
328
328
|
|
|
329
329
|
return () => {
|
|
330
330
|
chart.onInteractorPointerDown = null;
|
|
331
331
|
chart.onInteractorPointerMove = null;
|
|
332
332
|
chart.onInteractorPointerUp = null;
|
|
333
|
+
chart.onInteractorDragEnd = null;
|
|
333
334
|
chart.activators.delete(activateHoveredAxis);
|
|
334
335
|
};
|
|
335
336
|
});
|
|
336
337
|
</script>
|
|
337
338
|
|
|
338
|
-
<svelte:window onkeydown={handleKeyDown} onkeyup={handleKeyUp} />
|
|
339
|
+
<svelte:window onkeydown={handleKeyDown} onkeyup={handleKeyUp} onblur={handleBlur} />
|
|
339
340
|
|
|
340
341
|
{#if ps}
|
|
341
342
|
{#each ps.columns as col (col)}
|
|
@@ -356,8 +357,7 @@
|
|
|
356
357
|
width={brushHalfWidth * 2}
|
|
357
358
|
height={screen.size}
|
|
358
359
|
orientation="y"
|
|
359
|
-
|
|
360
|
-
onResizeStart={(edge, e) => handleResizeStart(screen.col, edge, e)}
|
|
360
|
+
owner={screen.col}
|
|
361
361
|
onWheel={() => {}}
|
|
362
362
|
/>
|
|
363
363
|
{/each}
|
|
@@ -4,10 +4,30 @@ export declare const EPS_ABSOLUTE = 1e-12;
|
|
|
4
4
|
export declare const DRAG_THRESHOLD = 3;
|
|
5
5
|
export declare const WHEEL_SENSITIVITY = 0.001;
|
|
6
6
|
export declare function closeTo(a: number | null | undefined, b: number | null | undefined): boolean;
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Space-held pan mode for brushes, wired to <svelte:window> keydown, keyup and
|
|
9
|
+
* blur. Each change calls `reroutePress` (the chart's), so a press already
|
|
10
|
+
* under way switches between panning and brushing where the pointer is now:
|
|
11
|
+
* with three-finger drag on a trackpad, one press can last seconds after the
|
|
12
|
+
* fingers lift. Key repeats while Space is held change nothing. Blur ends pan
|
|
13
|
+
* mode: a Space released while the window is unfocused sends no keyup.
|
|
14
|
+
*/
|
|
15
|
+
export declare function createPanModeHandlers(setActive: (active: boolean) => void, reroutePress: () => void): {
|
|
8
16
|
handleKeyDown: (event: KeyboardEvent) => void;
|
|
9
17
|
handleKeyUp: (event: KeyboardEvent) => void;
|
|
18
|
+
handleBlur: () => void;
|
|
10
19
|
};
|
|
20
|
+
/** The parts of a brush overlay a press can land on, from its `data-brush-handle`. */
|
|
21
|
+
export type BrushHandle = 'move' | 'left' | 'right' | 'top' | 'bottom' | 'nw' | 'ne' | 'sw' | 'se';
|
|
22
|
+
/**
|
|
23
|
+
* The brush handle under a press, read from the overlay's DOM: its element's
|
|
24
|
+
* `data-brush-handle`, and the `data-brush-owner` of its overlay (a parallel
|
|
25
|
+
* axis's column), or null when the press is not on a brush.
|
|
26
|
+
*/
|
|
27
|
+
export declare function brushHandleAt(target: Element | null): {
|
|
28
|
+
handle: BrushHandle;
|
|
29
|
+
owner: string | null;
|
|
30
|
+
} | null;
|
|
11
31
|
export declare function scaleBrushExtent1D(min: number, max: number, scaleFactor: number): [number, number];
|
|
12
32
|
export declare function scaleBrushExtent2D(xMin: number, xMax: number, yMin: number, yMax: number, scaleFactor: number): {
|
|
13
33
|
x: [number, number];
|
|
@@ -15,18 +15,46 @@ export function closeTo(a, b) {
|
|
|
15
15
|
}
|
|
16
16
|
return diff < EPS_ABSOLUTE;
|
|
17
17
|
}
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Space-held pan mode for brushes, wired to <svelte:window> keydown, keyup and
|
|
20
|
+
* blur. Each change calls `reroutePress` (the chart's), so a press already
|
|
21
|
+
* under way switches between panning and brushing where the pointer is now:
|
|
22
|
+
* with three-finger drag on a trackpad, one press can last seconds after the
|
|
23
|
+
* fingers lift. Key repeats while Space is held change nothing. Blur ends pan
|
|
24
|
+
* mode: a Space released while the window is unfocused sends no keyup.
|
|
25
|
+
*/
|
|
26
|
+
export function createPanModeHandlers(setActive, reroutePress) {
|
|
27
|
+
let active = false;
|
|
28
|
+
const set = (next) => {
|
|
29
|
+
if (next === active)
|
|
30
|
+
return;
|
|
31
|
+
active = next;
|
|
32
|
+
setActive(next);
|
|
33
|
+
reroutePress();
|
|
34
|
+
};
|
|
19
35
|
const handleKeyDown = (event) => {
|
|
20
|
-
if (event.code === 'Space')
|
|
21
|
-
|
|
22
|
-
}
|
|
36
|
+
if (event.code === 'Space')
|
|
37
|
+
set(true);
|
|
23
38
|
};
|
|
24
39
|
const handleKeyUp = (event) => {
|
|
25
|
-
if (event.code === 'Space')
|
|
26
|
-
|
|
27
|
-
|
|
40
|
+
if (event.code === 'Space')
|
|
41
|
+
set(false);
|
|
42
|
+
};
|
|
43
|
+
return { handleKeyDown, handleKeyUp, handleBlur: () => set(false) };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The brush handle under a press, read from the overlay's DOM: its element's
|
|
47
|
+
* `data-brush-handle`, and the `data-brush-owner` of its overlay (a parallel
|
|
48
|
+
* axis's column), or null when the press is not on a brush.
|
|
49
|
+
*/
|
|
50
|
+
export function brushHandleAt(target) {
|
|
51
|
+
const el = target?.closest?.('[data-brush-handle]');
|
|
52
|
+
if (!el)
|
|
53
|
+
return null;
|
|
54
|
+
return {
|
|
55
|
+
handle: el.getAttribute('data-brush-handle'),
|
|
56
|
+
owner: el.closest('[data-brush-owner]')?.getAttribute('data-brush-owner') ?? null
|
|
28
57
|
};
|
|
29
|
-
return { handleKeyDown, handleKeyUp };
|
|
30
58
|
}
|
|
31
59
|
export function scaleBrushExtent1D(min, max, scaleFactor) {
|
|
32
60
|
const center = (min + max) / 2;
|
|
@@ -5,13 +5,23 @@
|
|
|
5
5
|
* import { setQueryContext } from './';
|
|
6
6
|
* setQueryContext(wasm.getConnection()); // Promise OK — resolves lazily
|
|
7
7
|
*
|
|
8
|
-
* Usage
|
|
9
|
-
*
|
|
10
|
-
* context.set('
|
|
8
|
+
* Usage outside a component (the spec renderer), passing contexts to mount():
|
|
9
|
+
* const { filterGroups } = createQueryServices(queryEngine);
|
|
10
|
+
* context.set('query-engine', queryEngine);
|
|
11
|
+
* context.set('filter-groups', filterGroups);
|
|
11
12
|
*/
|
|
12
13
|
import { QueryEngine } from './QueryEngine';
|
|
13
14
|
import type { DuckDBConnection } from './QueryEngine';
|
|
14
15
|
import { FilterGroupRegistry } from './FilterGroupRegistry.svelte';
|
|
16
|
+
/**
|
|
17
|
+
* Create the FilterGroupRegistry for an engine and wire both to the schema
|
|
18
|
+
* service. Brushes and toggles reach the engine only through this wiring:
|
|
19
|
+
* a FilterGroup without an engine updates its selection but never re-queries.
|
|
20
|
+
*/
|
|
21
|
+
export declare function createQueryServices(queryEngine: QueryEngine): {
|
|
22
|
+
queryEngine: QueryEngine;
|
|
23
|
+
filterGroups: FilterGroupRegistry;
|
|
24
|
+
};
|
|
15
25
|
/**
|
|
16
26
|
* Create QueryEngine + FilterGroupRegistry and set Svelte contexts.
|
|
17
27
|
* Call this synchronously in a component's <script> block.
|