@fjader/svelte 0.2.3 → 0.2.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.
- package/README.md +3 -1
- package/dist/animation/EntranceGate.d.ts +52 -0
- package/dist/animation/EntranceGate.js +93 -0
- package/dist/animation/TransitionClock.d.ts +61 -30
- package/dist/animation/TransitionClock.js +67 -29
- package/dist/animation/transition-log.d.ts +41 -0
- package/dist/animation/transition-log.js +57 -0
- package/dist/components/axes/AxisBottom.svelte +27 -8
- package/dist/components/axes/AxisBottom.svelte.d.ts +10 -0
- package/dist/components/axes/AxisCenter.svelte +4 -2
- package/dist/components/axes/AxisLeft.svelte +81 -10
- package/dist/components/axes/AxisLeft.svelte.d.ts +17 -0
- package/dist/components/axes/BandAxisTicks.svelte +4 -3
- package/dist/components/axes/axis-title.d.ts +30 -0
- package/dist/components/axes/axis-title.js +77 -0
- package/dist/components/charts/ChartCell.svelte +33 -6
- package/dist/components/charts/ChartCell.svelte.d.ts +9 -0
- package/dist/components/charts/ChartContainer.svelte +55 -5
- package/dist/components/charts/ChartContainer.svelte.d.ts +14 -1
- package/dist/components/charts/ChartHover.svelte.d.ts +14 -0
- package/dist/components/charts/ChartHover.svelte.js +22 -0
- package/dist/components/charts/ChartVisibility.svelte.d.ts +43 -0
- package/dist/components/charts/ChartVisibility.svelte.js +67 -0
- package/dist/components/guides/Grid.svelte +12 -1
- package/dist/components/guides/legend/ContinuousColorLegend.svelte +7 -1
- package/dist/components/guides/legend/OrdinalColorLegend.svelte +8 -5
- package/dist/components/marks/AreaMarkX.svelte +18 -4
- package/dist/components/marks/AreaMarkY.svelte +18 -4
- package/dist/components/marks/BarMarkX.svelte +20 -4
- package/dist/components/marks/BarMarkY.svelte +20 -4
- package/dist/components/marks/DotsMark.svelte +8 -3
- package/dist/components/marks/LineMark.svelte +13 -6
- package/dist/components/marks/ParallelCoordinatesMark.svelte +11 -2
- package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +4 -0
- package/dist/components/marks/RectMark.svelte +8 -3
- package/dist/components/marks/RectXMark.svelte +9 -3
- package/dist/components/marks/RectYMark.svelte +9 -3
- package/dist/components/marks/RuleMarkX.svelte +31 -25
- package/dist/components/marks/RuleMarkY.svelte +31 -25
- package/dist/components/marks/TextMark.svelte +52 -14
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +22 -1
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +579 -66
- package/dist/components/marks/lifecycle/domain-queries.d.ts +13 -2
- package/dist/components/marks/lifecycle/domain-queries.js +43 -11
- package/dist/components/marks/lifecycle/domain-registration.d.ts +6 -1
- package/dist/components/marks/lifecycle/domain-registration.js +17 -20
- package/dist/components/marks/lifecycle/other-rows.svelte.d.ts +112 -0
- package/dist/components/marks/lifecycle/other-rows.svelte.js +193 -0
- package/dist/components/marks/lifecycle/other-split.d.ts +80 -0
- package/dist/components/marks/lifecycle/other-split.js +112 -0
- package/dist/components/marks/lifecycle/query-builder.d.ts +53 -3
- package/dist/components/marks/lifecycle/query-builder.js +46 -23
- package/dist/components/marks/utils/MarkColor.svelte.d.ts +3 -1
- package/dist/components/marks/utils/MarkColor.svelte.js +33 -3
- package/dist/components/marks/utils/MarkSeries.svelte.d.ts +14 -1
- package/dist/components/marks/utils/MarkSeries.svelte.js +25 -1
- package/dist/components/marks/utils/MarkSize.svelte.js +4 -0
- package/dist/components/marks/utils/reactive-config.d.ts +1 -1
- package/dist/components/marks/utils/reactive-config.js +9 -0
- package/dist/components/marks/utils/tooltip-helpers.d.ts +4 -1
- package/dist/components/marks/utils/tooltip-helpers.js +2 -1
- package/dist/components/rendering/Areas.svelte +57 -19
- package/dist/components/rendering/Areas.svelte.d.ts +12 -0
- package/dist/components/rendering/BaseGPURenderer.svelte.d.ts +3 -1
- package/dist/components/rendering/BaseGPURenderer.svelte.js +4 -2
- package/dist/components/rendering/Dots.svelte +23 -3
- package/dist/components/rendering/Dots.svelte.d.ts +6 -0
- package/dist/components/rendering/GridLine.svelte +13 -1
- package/dist/components/rendering/GridLine.svelte.d.ts +5 -0
- package/dist/components/rendering/Lines.svelte +48 -13
- package/dist/components/rendering/Lines.svelte.d.ts +12 -0
- package/dist/components/rendering/ParallelLines.svelte +5 -5
- package/dist/components/rendering/Rects.svelte +164 -30
- package/dist/components/rendering/Rects.svelte.d.ts +23 -2
- package/dist/components/rendering/RuleX.svelte +1 -1
- package/dist/components/rendering/RuleY.svelte +1 -1
- package/dist/components/rendering/WebGPUCanvas.svelte +4 -2
- package/dist/components/rendering/activity-reads.d.ts +47 -12
- package/dist/components/rendering/activity-reads.js +61 -19
- package/dist/components/rendering/uniform-sizes.d.ts +1 -1
- package/dist/components/rendering/uniform-sizes.js +1 -1
- package/dist/config/ChartConfig.svelte.d.ts +10 -1
- package/dist/config/ChartConfig.svelte.js +17 -0
- package/dist/gpu/GPUCoordinator.svelte.d.ts +31 -1
- package/dist/gpu/GPUCoordinator.svelte.js +164 -7
- package/dist/gpu/MorphedChannels.d.ts +11 -28
- package/dist/gpu/MorphedChannels.js +72 -8
- package/dist/gpu/ScaleTemplateSystem.d.ts +20 -2
- package/dist/gpu/ScaleTemplateSystem.js +83 -6
- package/dist/index.d.ts +19 -4
- package/dist/index.js +22 -4
- package/dist/interactors/Interval1D.svelte +6 -1
- package/dist/interactors/Interval2D.svelte +6 -1
- package/dist/interactors/ParallelAxisSource.svelte +10 -1
- package/dist/interactors/Toggle.svelte +32 -3
- package/dist/interactors/Tooltip.svelte +138 -41
- package/dist/interactors/TooltipManager.svelte.d.ts +20 -5
- package/dist/interactors/TooltipManager.svelte.js +46 -12
- package/dist/interactors/other-tooltip.d.ts +16 -0
- package/dist/interactors/other-tooltip.js +22 -0
- package/dist/query-engine/CrossfilterSelection.svelte.d.ts +4 -2
- package/dist/query-engine/CrossfilterSelection.svelte.js +2 -0
- package/dist/query-engine/FieldActivity.svelte.d.ts +49 -1
- package/dist/query-engine/FieldActivity.svelte.js +175 -5
- package/dist/query-engine/FilterGroup.svelte.d.ts +23 -1
- package/dist/query-engine/FilterGroup.svelte.js +43 -3
- package/dist/query-engine/FilterGroupRegistry.svelte.d.ts +31 -3
- package/dist/query-engine/FilterGroupRegistry.svelte.js +62 -15
- package/dist/query-engine/MarkHandle.svelte.d.ts +28 -1
- package/dist/query-engine/MarkHandle.svelte.js +33 -1
- package/dist/query-engine/QueryEngine.d.ts +225 -5
- package/dist/query-engine/QueryEngine.js +697 -47
- package/dist/query-engine/categorical-identity.d.ts +41 -0
- package/dist/query-engine/categorical-identity.js +145 -65
- package/dist/query-engine/debug/query-log.d.ts +39 -0
- package/dist/query-engine/debug/query-log.js +90 -0
- package/dist/query-engine/duckdb.d.ts +55 -8
- package/dist/query-engine/duckdb.js +82 -30
- package/dist/query-engine/index.d.ts +7 -4
- package/dist/query-engine/index.js +3 -3
- package/dist/query-engine/preagg/BuildQueue.d.ts +33 -13
- package/dist/query-engine/preagg/BuildQueue.js +51 -15
- package/dist/query-engine/preagg/Preagg.d.ts +46 -3
- package/dist/query-engine/preagg/Preagg.js +159 -31
- package/dist/query-engine/preagg/plan.d.ts +33 -1
- package/dist/query-engine/preagg/plan.js +158 -14
- package/dist/query-engine/sliced-lane.d.ts +108 -0
- package/dist/query-engine/sliced-lane.js +299 -0
- package/dist/query-engine/source-routing.d.ts +106 -0
- package/dist/query-engine/source-routing.js +189 -0
- package/dist/query-engine/toggle-predicate.d.ts +21 -0
- package/dist/query-engine/toggle-predicate.js +29 -1
- package/dist/scales/ActivityTable.d.ts +5 -1
- package/dist/scales/ActivityTable.js +12 -3
- package/dist/scales/BandScaleState.svelte.d.ts +22 -9
- package/dist/scales/BandScaleState.svelte.js +39 -21
- package/dist/scales/ChartScales.svelte.d.ts +3 -0
- package/dist/scales/ChartScales.svelte.js +26 -8
- package/dist/scales/ColorScaleState.svelte.d.ts +18 -2
- package/dist/scales/ColorScaleState.svelte.js +138 -26
- package/dist/scales/LinearScaleState.svelte.d.ts +7 -10
- package/dist/scales/LinearScaleState.svelte.js +21 -39
- package/dist/scales/ScaleGeneration.svelte.d.ts +48 -0
- package/dist/scales/ScaleGeneration.svelte.js +42 -0
- package/dist/scales/ScaleRegistry.svelte.js +3 -1
- package/dist/scales/SlotTransition.d.ts +15 -1
- package/dist/scales/SlotTransition.js +33 -4
- package/dist/scales/TimeScaleState.svelte.d.ts +7 -3
- package/dist/scales/TimeScaleState.svelte.js +7 -7
- package/dist/scales/color-utils.d.ts +0 -3
- package/dist/scales/color-utils.js +0 -3
- package/dist/scales/entered.d.ts +8 -0
- package/dist/scales/entered.js +10 -0
- package/dist/spec/LazyQueryEngine.js +2 -19
- package/dist/spec/MarkRegistry.d.ts +3 -1
- package/dist/spec/MarkRegistry.js +13 -1
- package/dist/spec/PlotFromSpec.svelte +1 -0
- package/dist/spec/export.js +11 -2
- package/dist/spec/types.d.ts +43 -13
- package/dist/sql/bin.d.ts +25 -0
- package/dist/sql/bin.js +108 -5
- package/dist/theme.css +5 -4
- package/dist/transforms/TransformCompiler.d.ts +47 -2
- package/dist/transforms/TransformCompiler.js +70 -1
- package/dist/transforms/index.js +176 -42
- package/dist/types.d.ts +177 -2
- package/package.json +4 -3
|
@@ -178,10 +178,10 @@ const SCALE_FRAGMENTS = {
|
|
|
178
178
|
rangeMax
|
|
179
179
|
);
|
|
180
180
|
|
|
181
|
-
// Return center of band
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
|
|
181
|
+
// Return center of band, slot 0 at the start of the range on both
|
|
182
|
+
// axes: the left on x, the top on y, so a y band axis reads top-down
|
|
183
|
+
// like a list and its exit (one past the last slot) is the list's end.
|
|
184
|
+
return rangeMin + bandLayout.x + bandLayout.y * 0.5;
|
|
185
185
|
}
|
|
186
186
|
`,
|
|
187
187
|
bufferLayout: {
|
|
@@ -280,14 +280,14 @@ function _scaleFunctionName(scaleType, axis) {
|
|
|
280
280
|
}
|
|
281
281
|
/**
|
|
282
282
|
* Bytes of immediate data every render pipeline declares: the chart's x and y
|
|
283
|
-
* `ScaleParam`, 16 bytes each. Every layout declares the same size because a
|
|
283
|
+
* `ScaleParam`, 16 bytes each, and its entrance, a vec4. Every layout declares the same size because a
|
|
284
284
|
* draw requires every declared byte to have been set, and the pass sets them
|
|
285
285
|
* once, before its first draw.
|
|
286
286
|
*
|
|
287
287
|
* Immediates carry state shared across the pass and read by several marks: the
|
|
288
288
|
* chart's scales and settings. 64 bytes are available on M-series Macs.
|
|
289
289
|
*/
|
|
290
|
-
export const CHART_SCALE_IMMEDIATE_SIZE =
|
|
290
|
+
export const CHART_SCALE_IMMEDIATE_SIZE = 48;
|
|
291
291
|
/**
|
|
292
292
|
* Main template system class
|
|
293
293
|
*/
|
|
@@ -376,6 +376,7 @@ export class ScaleTemplateSystem {
|
|
|
376
376
|
// ===== 3. HELPER FUNCTIONS =====
|
|
377
377
|
|
|
378
378
|
${this._buildSlotTableHelpers()}
|
|
379
|
+
${this._buildEntranceHelpers(scaleTypes)}
|
|
379
380
|
${this._buildBandActivityHelpers(scaleTypes)}
|
|
380
381
|
${bandHelpers}
|
|
381
382
|
${implementations}
|
|
@@ -413,6 +414,10 @@ export class ScaleTemplateSystem {
|
|
|
413
414
|
struct ChartScales {
|
|
414
415
|
x: ScaleParam,
|
|
415
416
|
y: ScaleParam,
|
|
417
|
+
// The chart's entrance, written per pass from its entrance clock:
|
|
418
|
+
// x = how far a growing mark has grown, y = a growing mark's opacity,
|
|
419
|
+
// z = a fading mark's opacity. All 1 once the chart has entered.
|
|
420
|
+
entrance: vec4<f32>,
|
|
416
421
|
};
|
|
417
422
|
var<immediate> chartScales: ChartScales;
|
|
418
423
|
|
|
@@ -448,6 +453,57 @@ export class ScaleTemplateSystem {
|
|
|
448
453
|
`;
|
|
449
454
|
return `${structDef}\n${chartScaleDeclaration}`;
|
|
450
455
|
}
|
|
456
|
+
/**
|
|
457
|
+
* The chart's entrance, for marks to apply in the vertex stage.
|
|
458
|
+
*
|
|
459
|
+
* A mark either GROWS along one axis (bars, histograms and areas, from the
|
|
460
|
+
* value 0 on their measure axis) or FADES (everything else). Which one is
|
|
461
|
+
* the mark's to say, as `axis`: 0 fades, 1 grows along x, 2 along y. The
|
|
462
|
+
* chart's mode is already folded into the immediates, so a chart set to
|
|
463
|
+
* fade hands a growing mark g = 1 and its opacity instead.
|
|
464
|
+
*
|
|
465
|
+
* The base a mark grows from is where 0 sits on screen, clamped to the plot:
|
|
466
|
+
* a domain that leaves 0 out grows from the edge, which is where its bars
|
|
467
|
+
* visibly start anyway. A log axis has no 0 and grows from its low edge.
|
|
468
|
+
* Decided per scale type here, so no shader evaluates log(0).
|
|
469
|
+
*/
|
|
470
|
+
_buildEntranceHelpers(scaleTypes) {
|
|
471
|
+
const base = (axis, type) => {
|
|
472
|
+
const a = axis.toLowerCase();
|
|
473
|
+
const lo = `frameUniforms.rangeMin.${a}`;
|
|
474
|
+
const hi = `frameUniforms.rangeMax.${a}`;
|
|
475
|
+
if (type === 'band' || type === 'point')
|
|
476
|
+
return '0.0';
|
|
477
|
+
// Screen y runs downward: the low edge of a y axis is rangeMax.
|
|
478
|
+
if (type === 'log')
|
|
479
|
+
return axis === 'X' ? lo : hi;
|
|
480
|
+
return `clamp(${_scaleFunctionName(type, axis)}From(0.0, chartScale${axis}()), ${lo}, ${hi})`;
|
|
481
|
+
};
|
|
482
|
+
return `
|
|
483
|
+
// ===== ENTRANCE =====
|
|
484
|
+
fn entranceBaseX() -> f32 {
|
|
485
|
+
return ${base('X', scaleTypes.x)};
|
|
486
|
+
}
|
|
487
|
+
fn entranceBaseY() -> f32 {
|
|
488
|
+
return ${base('Y', scaleTypes.y)};
|
|
489
|
+
}
|
|
490
|
+
// A screen position as it is during the entrance: pulled toward the base
|
|
491
|
+
// on the growing axis, untouched otherwise.
|
|
492
|
+
fn entrancePosition(p: vec2<f32>, axis: u32) -> vec2<f32> {
|
|
493
|
+
let g = chartScales.entrance.x;
|
|
494
|
+
if (axis == 1u) {
|
|
495
|
+
return vec2<f32>(mix(entranceBaseX(), p.x, g), p.y);
|
|
496
|
+
}
|
|
497
|
+
if (axis == 2u) {
|
|
498
|
+
return vec2<f32>(p.x, mix(entranceBaseY(), p.y, g));
|
|
499
|
+
}
|
|
500
|
+
return p;
|
|
501
|
+
}
|
|
502
|
+
fn entranceAlpha(axis: u32) -> f32 {
|
|
503
|
+
return select(chartScales.entrance.z, chartScales.entrance.y, axis != 0u);
|
|
504
|
+
}
|
|
505
|
+
`;
|
|
506
|
+
}
|
|
451
507
|
/**
|
|
452
508
|
* `bandActivityX(value, group)` and `bandActivityY(value, group)`: how
|
|
453
509
|
* selected a band or point axis's category is in the mark's filter group,
|
|
@@ -465,6 +521,9 @@ export class ScaleTemplateSystem {
|
|
|
465
521
|
if (type !== 'band' && type !== 'point') {
|
|
466
522
|
return `fn bandActivity${axis}(value: f32, group: u32) -> f32 {
|
|
467
523
|
return 1.0;
|
|
524
|
+
}
|
|
525
|
+
fn bandFreeActivity${axis}(group: u32) -> f32 {
|
|
526
|
+
return 1.0;
|
|
468
527
|
}`;
|
|
469
528
|
}
|
|
470
529
|
return `fn bandActivity${axis}(value: f32, group: u32) -> f32 {
|
|
@@ -472,6 +531,9 @@ export class ScaleTemplateSystem {
|
|
|
472
531
|
return 1.0;
|
|
473
532
|
}
|
|
474
533
|
return identityActivity(u32(chartScale${axis}().p4), group, u32(value));
|
|
534
|
+
}
|
|
535
|
+
fn bandFreeActivity${axis}(group: u32) -> f32 {
|
|
536
|
+
return identityFreeActivity(u32(chartScale${axis}().p4), group);
|
|
475
537
|
}`;
|
|
476
538
|
};
|
|
477
539
|
return `
|
|
@@ -554,6 +616,21 @@ export class ScaleTemplateSystem {
|
|
|
554
616
|
}
|
|
555
617
|
return identitySlots[actRef + n * group + id];
|
|
556
618
|
}
|
|
619
|
+
|
|
620
|
+
fn identityFreeActivity(base: u32, group: u32) -> f32 {
|
|
621
|
+
// The last slot of the group's run, past every id: 1 while the group
|
|
622
|
+
// selects nothing on the field, 0 while it selects anything, at the
|
|
623
|
+
// selection clock (FieldActivity's FREE SLOT). 1 without a block.
|
|
624
|
+
if (group == ${NO_GROUP}u) {
|
|
625
|
+
return 1.0;
|
|
626
|
+
}
|
|
627
|
+
let actRef = u32(identitySlots[base + 1u]);
|
|
628
|
+
let n = u32(identitySlots[base + 2u]);
|
|
629
|
+
if (n == 0u) {
|
|
630
|
+
return 1.0;
|
|
631
|
+
}
|
|
632
|
+
return identitySlots[actRef + n * group + n - 1u];
|
|
633
|
+
}
|
|
557
634
|
`;
|
|
558
635
|
}
|
|
559
636
|
_buildBandHelpers(scaleTypes) {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import type { DuckDBConnection } from './query-engine';
|
|
1
|
+
import type { DuckDBConnection, InitDuckDBOptions, QueryEngineOptions } from './query-engine';
|
|
2
|
+
export interface FjaderOptions extends InitDuckDBOptions {
|
|
3
|
+
/** An existing connection (or a promise of one); skips internal DuckDB-WASM init. */
|
|
4
|
+
connection?: DuckDBConnection | Promise<DuckDBConnection>;
|
|
5
|
+
/** Advanced: query engine tuning (see QueryEngineOptions). */
|
|
6
|
+
engine?: QueryEngineOptions;
|
|
7
|
+
}
|
|
8
|
+
type FjaderInit = DuckDBConnection | Promise<DuckDBConnection> | FjaderOptions;
|
|
2
9
|
/**
|
|
3
10
|
* Initialize Fjader: QueryEngine, ScaleRegistry, GPUCoordinator, FilterGroups.
|
|
4
11
|
* Call once in a top-level component's <script> block.
|
|
@@ -6,10 +13,14 @@ import type { DuckDBConnection } from './query-engine';
|
|
|
6
13
|
* import { initFjader } from '@fjader/svelte';
|
|
7
14
|
* const { queryEngine } = initFjader();
|
|
8
15
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
16
|
+
* Serve the DuckDB-WASM bundles yourself instead of loading them from jsDelivr:
|
|
17
|
+
* initFjader({ bundles: { mvp: { mainModule, mainWorker }, eh: { … } } });
|
|
18
|
+
*
|
|
19
|
+
* Pass an existing DuckDB connection to skip internal DuckDB-WASM init, e.g. to
|
|
20
|
+
* keep the AsyncDuckDB instance for registerFileBuffer:
|
|
21
|
+
* const { queryEngine } = initFjader(connectionFrom(await db.connect()));
|
|
11
22
|
*/
|
|
12
|
-
export declare function initFjader(
|
|
23
|
+
export declare function initFjader(init?: FjaderInit): {
|
|
13
24
|
queryEngine: import("./query-engine").QueryEngine;
|
|
14
25
|
filterGroups: import("./query-engine").FilterGroupRegistry;
|
|
15
26
|
scaleRegistry: import("./scales").ScaleRegistry;
|
|
@@ -19,5 +30,9 @@ export * from './components';
|
|
|
19
30
|
export * from './transforms';
|
|
20
31
|
export * from './sql/bin';
|
|
21
32
|
export * from './types';
|
|
33
|
+
export { getFilterGroups } from './query-engine';
|
|
34
|
+
export { connectionFrom } from './query-engine';
|
|
35
|
+
export type { DuckDBConnection, InitDuckDBOptions } from './query-engine';
|
|
36
|
+
export type { FilterGroupRegistry, FilterGroup, FilterItem } from './query-engine';
|
|
22
37
|
export { Fjader, render } from './spec';
|
|
23
38
|
export type { FjaderSpec, FjaderHandle, RenderOptions } from './spec';
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
import { setScaleRegistry } from './scales';
|
|
3
3
|
import { setGPUCoordinator } from './gpu';
|
|
4
4
|
import { setQueryContext, initDuckDB } from './query-engine';
|
|
5
|
+
/** A connection has query(), a promise has then(); anything else is options. */
|
|
6
|
+
function toOptions(init) {
|
|
7
|
+
if (!init)
|
|
8
|
+
return {};
|
|
9
|
+
if ('query' in init || 'then' in init)
|
|
10
|
+
return { connection: init };
|
|
11
|
+
return init;
|
|
12
|
+
}
|
|
5
13
|
/**
|
|
6
14
|
* Initialize Fjader: QueryEngine, ScaleRegistry, GPUCoordinator, FilterGroups.
|
|
7
15
|
* Call once in a top-level component's <script> block.
|
|
@@ -9,11 +17,16 @@ import { setQueryContext, initDuckDB } from './query-engine';
|
|
|
9
17
|
* import { initFjader } from '@fjader/svelte';
|
|
10
18
|
* const { queryEngine } = initFjader();
|
|
11
19
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
20
|
+
* Serve the DuckDB-WASM bundles yourself instead of loading them from jsDelivr:
|
|
21
|
+
* initFjader({ bundles: { mvp: { mainModule, mainWorker }, eh: { … } } });
|
|
22
|
+
*
|
|
23
|
+
* Pass an existing DuckDB connection to skip internal DuckDB-WASM init, e.g. to
|
|
24
|
+
* keep the AsyncDuckDB instance for registerFileBuffer:
|
|
25
|
+
* const { queryEngine } = initFjader(connectionFrom(await db.connect()));
|
|
14
26
|
*/
|
|
15
|
-
export function initFjader(
|
|
16
|
-
const {
|
|
27
|
+
export function initFjader(init) {
|
|
28
|
+
const { connection, bundles, engine } = toOptions(init);
|
|
29
|
+
const { queryEngine, filterGroups } = setQueryContext(connection ?? initDuckDB({ bundles }), engine);
|
|
17
30
|
const scaleRegistry = setScaleRegistry();
|
|
18
31
|
const gpuCoordinator = setGPUCoordinator({ msaaSampleCount: 4 });
|
|
19
32
|
gpuCoordinator.setScaleRegistry(scaleRegistry);
|
|
@@ -25,5 +38,10 @@ export * from './components';
|
|
|
25
38
|
export * from './transforms';
|
|
26
39
|
export * from './sql/bin';
|
|
27
40
|
export * from './types';
|
|
41
|
+
// Selection state: the registry initFjader() returns, for code outside it (is anything
|
|
42
|
+
// brushed? which groups exist?). Read from the same context.
|
|
43
|
+
export { getFilterGroups } from './query-engine';
|
|
44
|
+
// DuckDB: adapt an app-owned duckdb-wasm connection for initFjader(connection).
|
|
45
|
+
export { connectionFrom } from './query-engine';
|
|
28
46
|
// Spec-driven rendering API
|
|
29
47
|
export { Fjader, render } from './spec';
|
|
@@ -97,7 +97,12 @@
|
|
|
97
97
|
column: typeof resolvedField === 'string' ? resolvedField : (resolvedField?.toString() ?? ''),
|
|
98
98
|
label: typeof resolvedField === 'string' ? resolvedField : resolvedField?.toString(),
|
|
99
99
|
values: value ? [(value as number[])[0], (value as number[])[1]] : []
|
|
100
|
-
})
|
|
100
|
+
}),
|
|
101
|
+
// The clause was removed from outside (clearAll, a filter pill): take the
|
|
102
|
+
// rectangle down too, which only this control holds.
|
|
103
|
+
onCleared: () => {
|
|
104
|
+
if (brush.active) clearBrush();
|
|
105
|
+
}
|
|
101
106
|
});
|
|
102
107
|
|
|
103
108
|
// Pointer state (reused to avoid GC)
|
|
@@ -97,7 +97,12 @@
|
|
|
97
97
|
yColumn: resolvedYField,
|
|
98
98
|
xValues: (value as unknown[][])?.[0],
|
|
99
99
|
yValues: (value as unknown[][])?.[1]
|
|
100
|
-
})
|
|
100
|
+
}),
|
|
101
|
+
// The clause was removed from outside (clearAll, a filter pill): take the
|
|
102
|
+
// rectangle down too, which only this control holds.
|
|
103
|
+
onCleared: () => {
|
|
104
|
+
if (brush.active) clearBrush();
|
|
105
|
+
}
|
|
101
106
|
});
|
|
102
107
|
|
|
103
108
|
// Pointer state (reused to avoid GC)
|
|
@@ -83,7 +83,16 @@
|
|
|
83
83
|
column,
|
|
84
84
|
label: column,
|
|
85
85
|
values: value ? [(value as number[])[0], (value as number[])[1]] : []
|
|
86
|
-
})
|
|
86
|
+
}),
|
|
87
|
+
// The clause was removed from outside (clearAll, a filter pill): take this
|
|
88
|
+
// axis's rectangle down too, which only ParallelBrushes' state holds.
|
|
89
|
+
onCleared: () => {
|
|
90
|
+
const axis = axes.get(column);
|
|
91
|
+
if (!axis) return;
|
|
92
|
+
axis.brush.clear();
|
|
93
|
+
axis.lastEmitted.min = null;
|
|
94
|
+
axis.lastEmitted.max = null;
|
|
95
|
+
}
|
|
87
96
|
});
|
|
88
97
|
|
|
89
98
|
axes.set(column, { handle, brush: new BrushState1D(), lastEmitted: { min: null, max: null } });
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
function bandCategoryAt(
|
|
71
71
|
axis: 'x' | 'y',
|
|
72
72
|
innerPos: number
|
|
73
|
-
): { field: string; value: unknown } | null {
|
|
73
|
+
): { field: string; value: unknown; otherId: number | null } | null {
|
|
74
74
|
if (chart.scaleTypes?.[axis] !== 'band') return null;
|
|
75
75
|
const bandState = chart[`${axis}ScaleState`] as BandScaleState;
|
|
76
76
|
const extent = axis === 'x' ? chart.innerWidth : chart.innerHeight;
|
|
@@ -79,7 +79,28 @@
|
|
|
79
79
|
const id = bandState.idAtSlot(slot);
|
|
80
80
|
// undefined is "no category here"; null is the NULL category, a real one.
|
|
81
81
|
const value = id == null ? undefined : bandState.categoryAt(id);
|
|
82
|
-
|
|
82
|
+
if (value === undefined) return null;
|
|
83
|
+
// The axis's Other bucket: its label is no value, so the click selects
|
|
84
|
+
// what it holds instead.
|
|
85
|
+
const otherId = id != null && id === bandState.otherId ? id : null;
|
|
86
|
+
return { field: bandState.sourceField, value, otherId };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The Other bucket an inner click hit, or null: the hit row's band id on an
|
|
91
|
+
* axis of the declared field, when it is that axis's Other. The label in
|
|
92
|
+
* the row can't say, so the tooltip keeps the raw id beside it.
|
|
93
|
+
*/
|
|
94
|
+
function otherHit(point: Record<string, unknown>): number | null {
|
|
95
|
+
if (!declaredField) return null;
|
|
96
|
+
for (const axis of ['x', 'y'] as const) {
|
|
97
|
+
if (chart.scaleTypes?.[axis] !== 'band') continue;
|
|
98
|
+
const bandState = chart[`${axis}ScaleState`] as BandScaleState;
|
|
99
|
+
if (bandState.sourceField !== declaredField) continue;
|
|
100
|
+
const id = point[axis === 'x' ? '__bandIdX' : '__bandIdY'];
|
|
101
|
+
if (typeof id === 'number' && id === bandState.otherId) return id;
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
83
104
|
}
|
|
84
105
|
|
|
85
106
|
chart.onChartClick = (event) => {
|
|
@@ -91,6 +112,12 @@
|
|
|
91
112
|
// Path 1: Inner area click — use tooltip hit data (don't require visible —
|
|
92
113
|
// pointer micro-movement during click hides tooltip but data stays valid)
|
|
93
114
|
if (xInBounds && yInBounds && tm?.currentPoint) {
|
|
115
|
+
const otherId = otherHit(tm.currentPoint);
|
|
116
|
+
if (otherId !== null) {
|
|
117
|
+
const facetId = tm.currentPoint.__facetId;
|
|
118
|
+
declared.toggleOther(otherId, typeof facetId === 'number' ? facetId : null, isMulti);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
94
121
|
const tuple = fields.map((f) => tm.currentPoint![f]);
|
|
95
122
|
// A missing field is undefined; null is the NULL category, selectable.
|
|
96
123
|
if (tuple.some((v) => v === undefined)) return;
|
|
@@ -106,7 +133,9 @@
|
|
|
106
133
|
? bandCategoryAt('x', innerPoint.x)
|
|
107
134
|
: null;
|
|
108
135
|
if (hit) {
|
|
109
|
-
toggleFor(hit.field)
|
|
136
|
+
const toggle = toggleFor(hit.field);
|
|
137
|
+
if (hit.otherId !== null) toggle?.toggleOther(hit.otherId, null, isMulti);
|
|
138
|
+
else toggle?.toggle([hit.value], isMulti);
|
|
110
139
|
return;
|
|
111
140
|
}
|
|
112
141
|
|
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
Tooltip.svelte - Display component for data tooltips.
|
|
3
3
|
|
|
4
4
|
Reads from TooltipManager (via context) and renders a floating tooltip
|
|
5
|
-
at the cursor
|
|
6
|
-
|
|
5
|
+
at the cursor. It is a manual popover, so it paints in the top layer and no
|
|
6
|
+
overflow, scroll area, stacking context or transformed ancestor clips it. It
|
|
7
|
+
stays in the chart's DOM, so inherited theme variables and scoped styles still
|
|
8
|
+
apply. Placed in viewport pixels with the `translate` property (no layout per
|
|
9
|
+
move), flipped and clamped against the viewport, and measured by a
|
|
10
|
+
ResizeObserver rather than by reading its size.
|
|
7
11
|
|
|
8
12
|
Always renders one point at a time (stable layout). When multiple
|
|
9
13
|
points match, colored dots appear at the bottom and the user can
|
|
@@ -14,6 +18,7 @@
|
|
|
14
18
|
import { format as d3format } from 'd3-format';
|
|
15
19
|
import { categoryLabel } from '../query-engine/categorical-identity';
|
|
16
20
|
import type { ChartContext, ContinuousScaleState } from '../types';
|
|
21
|
+
import { otherTooltipRows } from './other-tooltip';
|
|
17
22
|
|
|
18
23
|
const commaFormat = d3format(',.4~g');
|
|
19
24
|
|
|
@@ -36,40 +41,76 @@
|
|
|
36
41
|
let colorField = $derived(tm?.currentColorField ?? null);
|
|
37
42
|
let channelFields = $derived(tm?.currentChannelFields ?? null);
|
|
38
43
|
let displayFields = $derived(tm?.currentDisplayFields ?? null);
|
|
39
|
-
let px = $derived(tm?.
|
|
40
|
-
let py = $derived(tm?.
|
|
44
|
+
let px = $derived(tm?.pointerClientX ?? 0);
|
|
45
|
+
let py = $derived(tm?.pointerClientY ?? 0);
|
|
41
46
|
|
|
42
|
-
//
|
|
43
|
-
|
|
47
|
+
// The tooltip's size, from a ResizeObserver: read after the browser's own
|
|
48
|
+
// layout, so measuring forces none. Its width is a high-water mark (grows,
|
|
49
|
+
// never shrinks) so it does not jitter between points: content wider than
|
|
50
|
+
// min-width makes the observed width exceed it, and min-width follows.
|
|
44
51
|
let minWidth = $state(0);
|
|
45
52
|
let tooltipW = $state(0);
|
|
46
53
|
let tooltipH = $state(0);
|
|
47
54
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
function measure(el: HTMLElement) {
|
|
56
|
+
const observer = new ResizeObserver((entries) => {
|
|
57
|
+
const box = entries[entries.length - 1].borderBoxSize[0];
|
|
58
|
+
// A closed popover is display: none and reports 0: keep the last size.
|
|
59
|
+
if (box.inlineSize === 0) return;
|
|
60
|
+
tooltipW = box.inlineSize;
|
|
61
|
+
tooltipH = box.blockSize;
|
|
62
|
+
if (box.inlineSize > minWidth) minWidth = box.inlineSize;
|
|
63
|
+
});
|
|
64
|
+
observer.observe(el);
|
|
65
|
+
return () => observer.disconnect();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// The viewport, without the scrollbars. Read when the tooltip opens and on
|
|
69
|
+
// resize, never per move.
|
|
70
|
+
let viewW = $state(0);
|
|
71
|
+
let viewH = $state(0);
|
|
72
|
+
function readViewport() {
|
|
73
|
+
const root = document.documentElement;
|
|
74
|
+
viewW = root.clientWidth;
|
|
75
|
+
viewH = root.clientHeight;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Open in the top layer while shown; closed again once it has faded out, so a
|
|
79
|
+
// page of charts keeps at most the hovered one's tooltip there.
|
|
80
|
+
function popover(el: HTMLElement) {
|
|
81
|
+
if (visible && !el.matches(':popover-open')) {
|
|
82
|
+
readViewport();
|
|
83
|
+
el.showPopover();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function handleTransitionEnd(event: TransitionEvent) {
|
|
87
|
+
const el = event.currentTarget as HTMLElement;
|
|
88
|
+
if (event.propertyName === 'opacity' && !visible && el.matches(':popover-open')) {
|
|
89
|
+
el.hidePopover();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
61
92
|
|
|
62
|
-
//
|
|
93
|
+
// Left of the cursor when it fits, else right; below it, else above. With
|
|
94
|
+
// hysteresis so it does not jitter at the edge. Then clamped into the viewport,
|
|
95
|
+
// for a tooltip that fits on neither side.
|
|
63
96
|
const GAP = 12;
|
|
64
97
|
const HYSTERESIS = 40;
|
|
65
|
-
|
|
66
|
-
let
|
|
67
|
-
|
|
98
|
+
const MARGIN = 8;
|
|
99
|
+
let leftOfCursor = true;
|
|
100
|
+
let aboveCursor = false;
|
|
101
|
+
const flippedX = $derived.by(() => {
|
|
68
102
|
const spaceLeft = px - tooltipW - GAP;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
flippedY = spaceY < 0 || (flippedY && spaceY < HYSTERESIS);
|
|
103
|
+
leftOfCursor = spaceLeft >= 0 && (leftOfCursor || spaceLeft >= HYSTERESIS);
|
|
104
|
+
return leftOfCursor;
|
|
72
105
|
});
|
|
106
|
+
const flippedY = $derived.by(() => {
|
|
107
|
+
const spaceBelow = viewH - py - tooltipH;
|
|
108
|
+
aboveCursor = spaceBelow < 0 || (aboveCursor && spaceBelow < HYSTERESIS);
|
|
109
|
+
return aboveCursor;
|
|
110
|
+
});
|
|
111
|
+
const clamp = (v: number, max: number) => Math.max(MARGIN, Math.min(v, max - MARGIN));
|
|
112
|
+
const left = $derived(clamp(flippedX ? px - tooltipW - GAP : px + GAP, viewW - tooltipW));
|
|
113
|
+
const top = $derived(clamp(flippedY ? py - tooltipH + 4 : py - 4, viewH - tooltipH));
|
|
73
114
|
|
|
74
115
|
// Transform origin: corner nearest the anchor point
|
|
75
116
|
let originX = $derived(flippedX ? '100%' : '0%');
|
|
@@ -95,7 +136,8 @@
|
|
|
95
136
|
if (!point) return [];
|
|
96
137
|
if (displayFields) return displayFields.filter((k) => k in point);
|
|
97
138
|
if (fields) return fields.filter((k) => k in point);
|
|
98
|
-
|
|
139
|
+
// Keys starting with `_` are kept for clicks (a band's raw id), never shown.
|
|
140
|
+
return Object.keys(point).filter((k) => !k.startsWith('_'));
|
|
99
141
|
});
|
|
100
142
|
|
|
101
143
|
// Scale-aware formatters for numeric values (same as MarginLabel)
|
|
@@ -110,6 +152,14 @@
|
|
|
110
152
|
: null
|
|
111
153
|
);
|
|
112
154
|
|
|
155
|
+
// An Other bar's rows, after the point's own: written with the value axis's format.
|
|
156
|
+
let otherRows = $derived.by(() => {
|
|
157
|
+
const stats = tm?.focusedOtherStats;
|
|
158
|
+
if (!stats || !point) return [];
|
|
159
|
+
const format = ('__bandIdX' in point ? yFormat : xFormat) ?? commaFormat;
|
|
160
|
+
return otherTooltipRows(stats, format);
|
|
161
|
+
});
|
|
162
|
+
|
|
113
163
|
function formatValue(key: string, value: unknown) {
|
|
114
164
|
if (formatters[key]) return formatters[key](value);
|
|
115
165
|
if (typeof value === 'number') {
|
|
@@ -123,20 +173,23 @@
|
|
|
123
173
|
|
|
124
174
|
const channelIcons: Record<string, string> = { x: '\u2192', y: '\u2191' };
|
|
125
175
|
|
|
126
|
-
// Computed style values — kept here so Prettier can't break them by line-wrapping
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const translate = $derived(`${
|
|
176
|
+
// Computed style values — kept here so Prettier can't break them by line-wrapping.
|
|
177
|
+
// `translate`, not `transform` or left/top: it moves the box without layout, and
|
|
178
|
+
// it applies after `scale`, so the pop-in scales about the box's own corner.
|
|
179
|
+
const translate = $derived(`${left}px ${top}px`);
|
|
130
180
|
const transformOrigin = $derived(`${originX} ${originY}`);
|
|
131
181
|
</script>
|
|
132
182
|
|
|
133
|
-
|
|
183
|
+
<svelte:window onresize={readViewport} />
|
|
184
|
+
|
|
185
|
+
<!-- In the top layer, placed in viewport pixels with flip detection -->
|
|
134
186
|
<div
|
|
135
187
|
class="tooltip"
|
|
136
188
|
class:visible
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
189
|
+
popover="manual"
|
|
190
|
+
{@attach measure}
|
|
191
|
+
{@attach popover}
|
|
192
|
+
ontransitionend={handleTransitionEnd}
|
|
140
193
|
style:translate
|
|
141
194
|
style:transform-origin={transformOrigin}
|
|
142
195
|
style:min-width="{minWidth}px"
|
|
@@ -155,6 +208,12 @@
|
|
|
155
208
|
</span>
|
|
156
209
|
</div>
|
|
157
210
|
{/each}
|
|
211
|
+
{#each otherRows as row (row.key)}
|
|
212
|
+
<div class="row">
|
|
213
|
+
<span class="key">{row.key}</span>
|
|
214
|
+
<span class="value">{row.value}</span>
|
|
215
|
+
</div>
|
|
216
|
+
{/each}
|
|
158
217
|
<div class="dots" class:show={isMulti}>
|
|
159
218
|
{#each matched as m, i (i)}
|
|
160
219
|
<span
|
|
@@ -168,24 +227,53 @@
|
|
|
168
227
|
|
|
169
228
|
<style>
|
|
170
229
|
.tooltip {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
230
|
+
/* A popover in the top layer: fixed to the viewport at its origin, placed by
|
|
231
|
+
translate. Resets the UA popover box (inset, margin, border, overflow). */
|
|
232
|
+
position: fixed;
|
|
233
|
+
inset: 0 auto auto 0;
|
|
234
|
+
margin: 0;
|
|
235
|
+
border: 0;
|
|
236
|
+
overflow: visible;
|
|
237
|
+
/* Colours, highest first: the app's --fjader-tooltip-bg/-text; the mode a
|
|
238
|
+
data-fjader-tooltip ancestor picks; contrast. light-dark() resolves against
|
|
239
|
+
the color-scheme the tooltip inherits from the chart's DOM ancestors. */
|
|
240
|
+
background: var(
|
|
241
|
+
--fjader-tooltip-bg,
|
|
242
|
+
var(--_fjader-tooltip-bg, light-dark(rgba(0, 0, 0, 0.88), rgba(255, 255, 255, 0.92)))
|
|
243
|
+
);
|
|
244
|
+
color: var(--fjader-tooltip-text, var(--_fjader-tooltip-text, light-dark(#ffffff, #111827)));
|
|
174
245
|
padding: 6px 10px;
|
|
175
246
|
border-radius: 4px;
|
|
176
247
|
font-family: var(--fjader-font, system-ui, sans-serif);
|
|
177
248
|
font-size: 12px;
|
|
178
249
|
line-height: 1.4;
|
|
179
250
|
pointer-events: none;
|
|
180
|
-
z-index: 1000;
|
|
181
251
|
white-space: nowrap;
|
|
182
|
-
box-shadow:
|
|
252
|
+
box-shadow:
|
|
253
|
+
0 0 0 1px var(--_fjader-tooltip-ring, transparent),
|
|
254
|
+
0 2px 8px rgba(0, 0, 0, 0.25);
|
|
183
255
|
opacity: 0;
|
|
184
256
|
scale: 0.78;
|
|
185
257
|
transition:
|
|
186
258
|
opacity 100ms ease-out,
|
|
187
259
|
scale 200ms cubic-bezier(0.34, 1.3, 0.64, 1);
|
|
188
|
-
will-change: opacity, scale;
|
|
260
|
+
will-change: opacity, scale, translate;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/* The mode, set on any ancestor (the page, a panel, a chart through ChartContainer's
|
|
264
|
+
tooltipScheme): custom properties inherit, so the nearest one wins. `contrast` is
|
|
265
|
+
the opposite of the scheme, `follow` the same, with a ring to separate it from a
|
|
266
|
+
background of its own colour. */
|
|
267
|
+
:global([data-fjader-tooltip='contrast']) {
|
|
268
|
+
--_fjader-tooltip-bg: light-dark(rgba(0, 0, 0, 0.88), rgba(255, 255, 255, 0.92));
|
|
269
|
+
--_fjader-tooltip-text: light-dark(#ffffff, #111827);
|
|
270
|
+
--_fjader-tooltip-ring: transparent;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
:global([data-fjader-tooltip='follow']) {
|
|
274
|
+
--_fjader-tooltip-bg: light-dark(rgba(255, 255, 255, 0.96), rgba(31, 41, 55, 0.96));
|
|
275
|
+
--_fjader-tooltip-text: light-dark(#111827, #f9fafb);
|
|
276
|
+
--_fjader-tooltip-ring: light-dark(rgba(17, 24, 39, 0.1), rgba(255, 255, 255, 0.12));
|
|
189
277
|
}
|
|
190
278
|
|
|
191
279
|
.tooltip.visible {
|
|
@@ -193,6 +281,15 @@
|
|
|
193
281
|
scale: 1;
|
|
194
282
|
}
|
|
195
283
|
|
|
284
|
+
/* Opening the popover takes it out of display: none, which runs no transition:
|
|
285
|
+
start the pop-in from the hidden look. */
|
|
286
|
+
@starting-style {
|
|
287
|
+
.tooltip.visible {
|
|
288
|
+
opacity: 0;
|
|
289
|
+
scale: 0.78;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
196
293
|
.row {
|
|
197
294
|
display: flex;
|
|
198
295
|
gap: 8px;
|