@fjader/svelte 0.2.2 → 0.2.4
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 +2 -0
- package/dist/components/axes/AxisBottom.svelte +23 -6
- package/dist/components/axes/AxisBottom.svelte.d.ts +10 -0
- package/dist/components/axes/AxisLeft.svelte +77 -8
- package/dist/components/axes/AxisLeft.svelte.d.ts +17 -0
- package/dist/components/axes/BandAxisTicks.svelte +64 -25
- package/dist/components/axes/BandAxisTicks.svelte.d.ts +5 -5
- 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 +1 -1
- package/dist/components/charts/ChartContainer.svelte +12 -3
- package/dist/components/charts/ChartContainer.svelte.d.ts +7 -0
- package/dist/components/guides/legend/ContinuousColorLegend.svelte +9 -2
- package/dist/components/guides/legend/OrdinalColorLegend.svelte +96 -35
- package/dist/components/marks/AreaMarkX.svelte +21 -6
- package/dist/components/marks/AreaMarkY.svelte +21 -6
- package/dist/components/marks/BarMarkX.svelte +16 -1
- package/dist/components/marks/BarMarkY.svelte +16 -1
- package/dist/components/marks/DotsMark.svelte +5 -0
- package/dist/components/marks/LineMark.svelte +8 -1
- package/dist/components/marks/ParallelCoordinatesMark.svelte +10 -0
- package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +5 -0
- package/dist/components/marks/RectMark.svelte +5 -0
- package/dist/components/marks/RectXMark.svelte +5 -0
- package/dist/components/marks/RectYMark.svelte +5 -0
- package/dist/components/marks/RuleMarkX.svelte +12 -2
- package/dist/components/marks/RuleMarkY.svelte +12 -2
- package/dist/components/marks/TextMark.svelte +76 -13
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +21 -2
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +300 -42
- package/dist/components/marks/lifecycle/domain-queries.d.ts +3 -1
- package/dist/components/marks/lifecycle/domain-queries.js +22 -15
- package/dist/components/marks/lifecycle/domain-registration.js +2 -6
- package/dist/components/marks/lifecycle/other-rows.svelte.d.ts +104 -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 +26 -1
- package/dist/components/marks/lifecycle/query-builder.js +25 -5
- package/dist/components/marks/lifecycle/transform-scale-key.d.ts +16 -0
- package/dist/components/marks/lifecycle/transform-scale-key.js +27 -0
- package/dist/components/marks/utils/area-outline.d.ts +24 -0
- package/dist/components/marks/utils/area-outline.js +10 -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 +30 -6
- package/dist/components/rendering/Areas.svelte.d.ts +2 -0
- package/dist/components/rendering/BaseGPURenderer.svelte.d.ts +3 -1
- package/dist/components/rendering/BaseGPURenderer.svelte.js +8 -3
- package/dist/components/rendering/Dots.svelte +24 -3
- package/dist/components/rendering/Dots.svelte.d.ts +2 -0
- package/dist/components/rendering/GridLine.svelte +2 -1
- package/dist/components/rendering/Lines.svelte +49 -7
- package/dist/components/rendering/Lines.svelte.d.ts +9 -0
- package/dist/components/rendering/ParallelLines.svelte +26 -7
- package/dist/components/rendering/ParallelLines.svelte.d.ts +2 -0
- package/dist/components/rendering/Rects.svelte +129 -20
- package/dist/components/rendering/Rects.svelte.d.ts +9 -0
- package/dist/components/rendering/RuleX.svelte +20 -5
- package/dist/components/rendering/RuleX.svelte.d.ts +2 -0
- package/dist/components/rendering/RuleY.svelte +20 -5
- package/dist/components/rendering/RuleY.svelte.d.ts +2 -0
- package/dist/components/rendering/activity-reads.d.ts +111 -0
- package/dist/components/rendering/activity-reads.js +154 -0
- package/dist/components/rendering/uniform-sizes.d.ts +26 -0
- package/dist/components/rendering/uniform-sizes.js +28 -0
- package/dist/config/ChartConfig.svelte.d.ts +5 -1
- package/dist/gpu/ColorScaleShaderLib.d.ts +13 -1
- package/dist/gpu/ColorScaleShaderLib.js +56 -5
- package/dist/gpu/GPUCoordinator.svelte.d.ts +8 -0
- package/dist/gpu/GPUCoordinator.svelte.js +68 -32
- package/dist/gpu/MorphedChannels.d.ts +6 -32
- package/dist/gpu/MorphedChannels.js +45 -3
- package/dist/gpu/ResolveShader.d.ts +2 -2
- package/dist/gpu/ResolveShader.js +2 -2
- package/dist/gpu/ScaleTemplateSystem.d.ts +19 -0
- package/dist/gpu/ScaleTemplateSystem.js +111 -11
- package/dist/gpu/SizeScaleShaderLib.d.ts +4 -4
- package/dist/gpu/SizeScaleShaderLib.js +4 -4
- package/dist/gpu/identity-slot-packing.d.ts +157 -40
- package/dist/gpu/identity-slot-packing.js +262 -62
- package/dist/gpu/tracked-scales.d.ts +14 -0
- package/dist/gpu/tracked-scales.js +18 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/interactors/BandLabelToggle.svelte +5 -9
- package/dist/interactors/BandLabelToggle.svelte.d.ts +3 -3
- package/dist/interactors/DerivedInterval1D.svelte +26 -54
- package/dist/interactors/Interval1D.svelte +8 -2
- package/dist/interactors/Interval2D.svelte +8 -2
- package/dist/interactors/ParallelAxisSource.svelte +14 -5
- package/dist/interactors/ParallelAxisSource.svelte.d.ts +2 -1
- package/dist/interactors/ParallelBrushes.svelte +8 -9
- package/dist/interactors/ParallelBrushes.svelte.d.ts +3 -3
- package/dist/interactors/Toggle.svelte +48 -38
- package/dist/interactors/Toggle.svelte.d.ts +4 -3
- package/dist/interactors/Tooltip.svelte +140 -42
- package/dist/interactors/TooltipManager.svelte.d.ts +20 -5
- package/dist/interactors/TooltipManager.svelte.js +40 -10
- package/dist/interactors/other-tooltip.d.ts +16 -0
- package/dist/interactors/other-tooltip.js +22 -0
- package/dist/interactors/relay-clauses.d.ts +23 -0
- package/dist/interactors/relay-clauses.js +60 -0
- package/dist/query-engine/CrossfilterSelection.svelte.d.ts +88 -15
- package/dist/query-engine/CrossfilterSelection.svelte.js +94 -20
- package/dist/query-engine/FieldActivity.svelte.d.ts +128 -0
- package/dist/query-engine/FieldActivity.svelte.js +378 -0
- package/dist/query-engine/FilterGroup.svelte.d.ts +76 -30
- package/dist/query-engine/FilterGroup.svelte.js +223 -124
- package/dist/query-engine/FilterGroupRegistry.svelte.d.ts +73 -3
- package/dist/query-engine/FilterGroupRegistry.svelte.js +161 -12
- package/dist/query-engine/MarkHandle.svelte.d.ts +9 -1
- package/dist/query-engine/MarkHandle.svelte.js +12 -2
- package/dist/query-engine/QueryEngine.d.ts +52 -16
- package/dist/query-engine/QueryEngine.js +116 -34
- package/dist/query-engine/categorical-identity.d.ts +114 -27
- package/dist/query-engine/categorical-identity.js +225 -51
- package/dist/query-engine/index.d.ts +2 -1
- package/dist/query-engine/index.js +1 -0
- package/dist/query-engine/preagg/Preagg.d.ts +1 -1
- package/dist/query-engine/preagg/Preagg.js +11 -9
- package/dist/query-engine/preagg/plan.d.ts +14 -5
- package/dist/query-engine/preagg/plan.js +44 -18
- package/dist/query-engine/toggle-predicate.d.ts +21 -0
- package/dist/query-engine/toggle-predicate.js +41 -2
- package/dist/scales/ActivityTable.d.ts +103 -0
- package/dist/scales/ActivityTable.js +220 -0
- package/dist/scales/BandScaleState.svelte.d.ts +41 -15
- package/dist/scales/BandScaleState.svelte.js +78 -36
- package/dist/scales/ChartScales.svelte.d.ts +2 -2
- package/dist/scales/ColorScaleState.svelte.d.ts +36 -8
- package/dist/scales/ColorScaleState.svelte.js +118 -24
- package/dist/scales/DomainResolver.d.ts +2 -0
- package/dist/scales/DomainResolver.js +15 -8
- package/dist/scales/ScaleRegistry.svelte.d.ts +9 -0
- package/dist/scales/ScaleRegistry.svelte.js +22 -1
- package/dist/scales/TimeScaleState.svelte.js +7 -1
- package/dist/spec/index.js +1 -0
- package/dist/spec/types.d.ts +4 -0
- package/dist/theme.css +5 -4
- package/dist/transforms/TransformCompiler.d.ts +37 -2
- package/dist/transforms/TransformCompiler.js +44 -1
- package/dist/transforms/index.d.ts +27 -0
- package/dist/transforms/index.js +127 -19
- package/dist/types.d.ts +71 -0
- package/package.json +3 -2
|
@@ -62,7 +62,8 @@
|
|
|
62
62
|
);
|
|
63
63
|
|
|
64
64
|
const brushHandle: SourceHandle = filterGroup.register(source, {
|
|
65
|
-
|
|
65
|
+
// Its own chart is not filtered by it (crossfilter).
|
|
66
|
+
chart: chart.config,
|
|
66
67
|
activeDims: () => {
|
|
67
68
|
const scale = getScale();
|
|
68
69
|
if (!scale || !columnRef) return [];
|
|
@@ -96,7 +97,12 @@
|
|
|
96
97
|
column: typeof resolvedField === 'string' ? resolvedField : (resolvedField?.toString() ?? ''),
|
|
97
98
|
label: typeof resolvedField === 'string' ? resolvedField : resolvedField?.toString(),
|
|
98
99
|
values: value ? [(value as number[])[0], (value as number[])[1]] : []
|
|
99
|
-
})
|
|
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
|
+
}
|
|
100
106
|
});
|
|
101
107
|
|
|
102
108
|
// Pointer state (reused to avoid GC)
|
|
@@ -54,7 +54,8 @@
|
|
|
54
54
|
state instanceof LinearScaleState ? state.domain.target : (scale.domain() as [number, number]);
|
|
55
55
|
|
|
56
56
|
const brushHandle: SourceHandle = filterGroup.register(source, {
|
|
57
|
-
|
|
57
|
+
// Its own chart is not filtered by it (crossfilter).
|
|
58
|
+
chart: chart.config,
|
|
58
59
|
activeDims: () => {
|
|
59
60
|
if (!xColumnRef || !yColumnRef) return [];
|
|
60
61
|
return [
|
|
@@ -96,7 +97,12 @@
|
|
|
96
97
|
yColumn: resolvedYField,
|
|
97
98
|
xValues: (value as unknown[][])?.[0],
|
|
98
99
|
yValues: (value as unknown[][])?.[1]
|
|
99
|
-
})
|
|
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
|
+
}
|
|
100
106
|
});
|
|
101
107
|
|
|
102
108
|
// Pointer state (reused to avoid GC)
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
filterGroup: FilterGroup;
|
|
38
38
|
column: string;
|
|
39
39
|
chartId: string;
|
|
40
|
-
|
|
40
|
+
/** The chart the brush skips, or null for a chart that filters its own lines. */
|
|
41
|
+
chart: object | null;
|
|
41
42
|
/** The axis's scale state now: removing another column renumbers the axes. */
|
|
42
43
|
scaleState: () => LinearScaleState | null;
|
|
43
44
|
/** The axes' pixel extent (the chart's inner height). */
|
|
@@ -47,13 +48,12 @@
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
// Keyed by column in the parent, so none of these change for an instance.
|
|
50
|
-
const { filterGroup, column, chartId,
|
|
51
|
-
$props();
|
|
51
|
+
const { filterGroup, column, chartId, chart, scaleState, pixelExtent, axes }: Props = $props();
|
|
52
52
|
|
|
53
53
|
const columnRef = sqlColumn(column);
|
|
54
54
|
|
|
55
55
|
const handle = filterGroup.register(`parallel-brush-${column}-${chartId}`, {
|
|
56
|
-
|
|
56
|
+
chart,
|
|
57
57
|
activeDims: () => {
|
|
58
58
|
const state = scaleState();
|
|
59
59
|
if (!state) return [];
|
|
@@ -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 } });
|
|
@@ -31,7 +31,8 @@ interface Props {
|
|
|
31
31
|
filterGroup: FilterGroup;
|
|
32
32
|
column: string;
|
|
33
33
|
chartId: string;
|
|
34
|
-
|
|
34
|
+
/** The chart the brush skips, or null for a chart that filters its own lines. */
|
|
35
|
+
chart: object | null;
|
|
35
36
|
/** The axis's scale state now: removing another column renumbers the axes. */
|
|
36
37
|
scaleState: () => LinearScaleState | null;
|
|
37
38
|
/** The axes' pixel extent (the chart's inner height). */
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
interface Props {
|
|
20
20
|
/** Filter group name — selects which FilterGroup these brushes write to. */
|
|
21
21
|
selection?: string;
|
|
22
|
-
/** When true, the
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
/** When true, the brushes skip their own chart (standard crossfilter
|
|
23
|
+
* behavior). When false (default), they filter it — typical for parallel
|
|
24
|
+
* coordinates. */
|
|
25
25
|
cross?: boolean;
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -105,11 +105,10 @@
|
|
|
105
105
|
return Math.min(30, (chart.innerWidth / (ps.columns.length - 1)) * 0.35);
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
-
// Cross-filter:
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
: () => new Set<string>();
|
|
108
|
+
// Cross-filter: with cross=true the brushes skip their own chart (standard
|
|
109
|
+
// crossfilter). With cross=false, the default for parallel coordinates, the
|
|
110
|
+
// chart filters its own lines.
|
|
111
|
+
const skips = cross ? chart.config : null;
|
|
113
112
|
|
|
114
113
|
/** An axis's scale state, looked up by column: removing a column renumbers the axes. */
|
|
115
114
|
function scaleStateOf(col: string): LinearScaleState | null {
|
|
@@ -344,7 +343,7 @@
|
|
|
344
343
|
{filterGroup}
|
|
345
344
|
column={col}
|
|
346
345
|
chartId={chart.chartId}
|
|
347
|
-
{
|
|
346
|
+
chart={skips}
|
|
348
347
|
scaleState={() => scaleStateOf(col)}
|
|
349
348
|
pixelExtent={() => chart.innerHeight}
|
|
350
349
|
{axes}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
interface Props {
|
|
2
2
|
/** Filter group name — selects which FilterGroup these brushes write to. */
|
|
3
3
|
selection?: string;
|
|
4
|
-
/** When true, the
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
/** When true, the brushes skip their own chart (standard crossfilter
|
|
5
|
+
* behavior). When false (default), they filter it — typical for parallel
|
|
6
|
+
* coordinates. */
|
|
7
7
|
cross?: boolean;
|
|
8
8
|
}
|
|
9
9
|
declare const ParallelBrushes: import("svelte").Component<Props, {}, "">;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { getContext, onDestroy } from 'svelte';
|
|
3
3
|
import type { ChartContext } from '../types';
|
|
4
|
-
import type {
|
|
5
|
-
import type { ActiveValueScale, ToggleHandle } from '../query-engine/FilterGroup.svelte';
|
|
4
|
+
import type { ToggleHandle } from '../query-engine/FilterGroup.svelte';
|
|
6
5
|
import type { FilterGroupRegistry } from '../query-engine/FilterGroupRegistry.svelte';
|
|
7
6
|
import type { BandScaleState } from '../scales/BandScaleState.svelte';
|
|
8
7
|
import BandLabelToggle from './BandLabelToggle.svelte';
|
|
@@ -16,9 +15,10 @@
|
|
|
16
15
|
* - Band axis label: toggles the axis's own source field, whatever the
|
|
17
16
|
* declared field is. Labels are a ready-made category picker.
|
|
18
17
|
*
|
|
19
|
-
* Toggles on different fields AND together. The
|
|
20
|
-
*
|
|
21
|
-
*
|
|
18
|
+
* Toggles on different fields AND together. The chart skips its own toggles
|
|
19
|
+
* (crossfilter): its marks that read the toggled field dim, the rest keep
|
|
20
|
+
* their data. Elsewhere in the group, marks that read the field dim and the
|
|
21
|
+
* rest filter.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
interface Props {
|
|
@@ -32,33 +32,18 @@
|
|
|
32
32
|
const filterGroups = getContext<FilterGroupRegistry>('filter-groups');
|
|
33
33
|
const filterGroup = filterGroups.get(selection);
|
|
34
34
|
const chart = getContext<ChartContext>('chart');
|
|
35
|
-
const scaleRegistry = getContext<ScaleRegistry>('scale-registry');
|
|
36
35
|
|
|
37
36
|
// Normalize field to array for multi-column support
|
|
38
37
|
const fields = Array.isArray(field) ? field : [field];
|
|
39
38
|
const declaredField = fields.length === 1 ? fields[0] : null;
|
|
40
39
|
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
function scalesFor(toggleField: string): ActiveValueScale[] {
|
|
47
|
-
const out: ActiveValueScale[] = [];
|
|
48
|
-
const color = scaleRegistry?.getScaleByField(toggleField, 'color');
|
|
49
|
-
if (color) out.push(color as unknown as ActiveValueScale);
|
|
50
|
-
for (const axis of ['x', 'y'] as const) {
|
|
51
|
-
if (chart.scaleTypes?.[axis] !== 'band') continue;
|
|
52
|
-
const band = chart[`${axis}ScaleState`] as BandScaleState | null | undefined;
|
|
53
|
-
if (band && band.sourceField === toggleField) out.push(band as unknown as ActiveValueScale);
|
|
54
|
-
}
|
|
55
|
-
return out;
|
|
56
|
-
}
|
|
40
|
+
/**
|
|
41
|
+
* This chart skips its own toggles (crossfilter): its marks keyed by the
|
|
42
|
+
* toggled field dim, the rest keep their data.
|
|
43
|
+
*/
|
|
44
|
+
const skips = chart.config;
|
|
57
45
|
|
|
58
|
-
const declared = filterGroup.registerToggle(fields, {
|
|
59
|
-
excludeMarkIds,
|
|
60
|
-
scales: () => (declaredField ? scalesFor(declaredField) : [])
|
|
61
|
-
});
|
|
46
|
+
const declared = filterGroup.registerToggle(fields, { chart: skips });
|
|
62
47
|
chart.activators.add(declared.activate);
|
|
63
48
|
|
|
64
49
|
/** One BandLabelToggle per band axis field (see bandFieldsOf). */
|
|
@@ -85,15 +70,37 @@
|
|
|
85
70
|
function bandCategoryAt(
|
|
86
71
|
axis: 'x' | 'y',
|
|
87
72
|
innerPos: number
|
|
88
|
-
): { field: string; value: unknown } | null {
|
|
73
|
+
): { field: string; value: unknown; otherId: number | null } | null {
|
|
89
74
|
if (chart.scaleTypes?.[axis] !== 'band') return null;
|
|
90
75
|
const bandState = chart[`${axis}ScaleState`] as BandScaleState;
|
|
91
76
|
const extent = axis === 'x' ? chart.innerWidth : chart.innerHeight;
|
|
92
77
|
const slot = bandState.invertScreenToData(innerPos, extent, axis === 'y');
|
|
93
78
|
if (slot == null) return null;
|
|
94
79
|
const id = bandState.idAtSlot(slot);
|
|
95
|
-
|
|
96
|
-
|
|
80
|
+
// undefined is "no category here"; null is the NULL category, a real one.
|
|
81
|
+
const value = id == null ? undefined : bandState.categoryAt(id);
|
|
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;
|
|
97
104
|
}
|
|
98
105
|
|
|
99
106
|
chart.onChartClick = (event) => {
|
|
@@ -105,8 +112,15 @@
|
|
|
105
112
|
// Path 1: Inner area click — use tooltip hit data (don't require visible —
|
|
106
113
|
// pointer micro-movement during click hides tooltip but data stays valid)
|
|
107
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
|
+
}
|
|
108
121
|
const tuple = fields.map((f) => tm.currentPoint![f]);
|
|
109
|
-
|
|
122
|
+
// A missing field is undefined; null is the NULL category, selectable.
|
|
123
|
+
if (tuple.some((v) => v === undefined)) return;
|
|
110
124
|
declared.toggle(tuple, isMulti);
|
|
111
125
|
return;
|
|
112
126
|
}
|
|
@@ -119,7 +133,9 @@
|
|
|
119
133
|
? bandCategoryAt('x', innerPoint.x)
|
|
120
134
|
: null;
|
|
121
135
|
if (hit) {
|
|
122
|
-
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);
|
|
123
139
|
return;
|
|
124
140
|
}
|
|
125
141
|
|
|
@@ -139,11 +155,5 @@
|
|
|
139
155
|
</script>
|
|
140
156
|
|
|
141
157
|
{#each bandFields as bandField (bandField)}
|
|
142
|
-
<BandLabelToggle
|
|
143
|
-
{filterGroup}
|
|
144
|
-
field={bandField}
|
|
145
|
-
{excludeMarkIds}
|
|
146
|
-
scales={() => scalesFor(bandField)}
|
|
147
|
-
handles={bandToggles}
|
|
148
|
-
/>
|
|
158
|
+
<BandLabelToggle {filterGroup} field={bandField} chart={skips} handles={bandToggles} />
|
|
149
159
|
{/each}
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
* - Band axis label: toggles the axis's own source field, whatever the
|
|
7
7
|
* declared field is. Labels are a ready-made category picker.
|
|
8
8
|
*
|
|
9
|
-
* Toggles on different fields AND together. The
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Toggles on different fields AND together. The chart skips its own toggles
|
|
10
|
+
* (crossfilter): its marks that read the toggled field dim, the rest keep
|
|
11
|
+
* their data. Elsewhere in the group, marks that read the field dim and the
|
|
12
|
+
* rest filter.
|
|
12
13
|
*/
|
|
13
14
|
interface Props {
|
|
14
15
|
/** Filter group name — selects which FilterGroup this toggle writes to. */
|
|
@@ -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
|
|
@@ -12,7 +16,9 @@
|
|
|
12
16
|
<script lang="ts">
|
|
13
17
|
import { getContext } from 'svelte';
|
|
14
18
|
import { format as d3format } from 'd3-format';
|
|
19
|
+
import { categoryLabel } from '../query-engine/categorical-identity';
|
|
15
20
|
import type { ChartContext, ContinuousScaleState } from '../types';
|
|
21
|
+
import { otherTooltipRows } from './other-tooltip';
|
|
16
22
|
|
|
17
23
|
const commaFormat = d3format(',.4~g');
|
|
18
24
|
|
|
@@ -35,40 +41,76 @@
|
|
|
35
41
|
let colorField = $derived(tm?.currentColorField ?? null);
|
|
36
42
|
let channelFields = $derived(tm?.currentChannelFields ?? null);
|
|
37
43
|
let displayFields = $derived(tm?.currentDisplayFields ?? null);
|
|
38
|
-
let px = $derived(tm?.
|
|
39
|
-
let py = $derived(tm?.
|
|
44
|
+
let px = $derived(tm?.pointerClientX ?? 0);
|
|
45
|
+
let py = $derived(tm?.pointerClientY ?? 0);
|
|
40
46
|
|
|
41
|
-
//
|
|
42
|
-
|
|
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.
|
|
43
51
|
let minWidth = $state(0);
|
|
44
52
|
let tooltipW = $state(0);
|
|
45
53
|
let tooltipH = $state(0);
|
|
46
54
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
+
}
|
|
60
92
|
|
|
61
|
-
//
|
|
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.
|
|
62
96
|
const GAP = 12;
|
|
63
97
|
const HYSTERESIS = 40;
|
|
64
|
-
|
|
65
|
-
let
|
|
66
|
-
|
|
98
|
+
const MARGIN = 8;
|
|
99
|
+
let leftOfCursor = true;
|
|
100
|
+
let aboveCursor = false;
|
|
101
|
+
const flippedX = $derived.by(() => {
|
|
67
102
|
const spaceLeft = px - tooltipW - GAP;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
flippedY = spaceY < 0 || (flippedY && spaceY < HYSTERESIS);
|
|
103
|
+
leftOfCursor = spaceLeft >= 0 && (leftOfCursor || spaceLeft >= HYSTERESIS);
|
|
104
|
+
return leftOfCursor;
|
|
71
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));
|
|
72
114
|
|
|
73
115
|
// Transform origin: corner nearest the anchor point
|
|
74
116
|
let originX = $derived(flippedX ? '100%' : '0%');
|
|
@@ -94,7 +136,8 @@
|
|
|
94
136
|
if (!point) return [];
|
|
95
137
|
if (displayFields) return displayFields.filter((k) => k in point);
|
|
96
138
|
if (fields) return fields.filter((k) => k in point);
|
|
97
|
-
|
|
139
|
+
// Keys starting with `_` are kept for clicks (a band's raw id), never shown.
|
|
140
|
+
return Object.keys(point).filter((k) => !k.startsWith('_'));
|
|
98
141
|
});
|
|
99
142
|
|
|
100
143
|
// Scale-aware formatters for numeric values (same as MarginLabel)
|
|
@@ -109,6 +152,14 @@
|
|
|
109
152
|
: null
|
|
110
153
|
);
|
|
111
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
|
+
|
|
112
163
|
function formatValue(key: string, value: unknown) {
|
|
113
164
|
if (formatters[key]) return formatters[key](value);
|
|
114
165
|
if (typeof value === 'number') {
|
|
@@ -117,25 +168,28 @@
|
|
|
117
168
|
if (fmt) return fmt(value);
|
|
118
169
|
return commaFormat(value);
|
|
119
170
|
}
|
|
120
|
-
return String(value);
|
|
171
|
+
return String(categoryLabel(value));
|
|
121
172
|
}
|
|
122
173
|
|
|
123
174
|
const channelIcons: Record<string, string> = { x: '\u2192', y: '\u2191' };
|
|
124
175
|
|
|
125
|
-
// Computed style values — kept here so Prettier can't break them by line-wrapping
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
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`);
|
|
129
180
|
const transformOrigin = $derived(`${originX} ${originY}`);
|
|
130
181
|
</script>
|
|
131
182
|
|
|
132
|
-
|
|
183
|
+
<svelte:window onresize={readViewport} />
|
|
184
|
+
|
|
185
|
+
<!-- In the top layer, placed in viewport pixels with flip detection -->
|
|
133
186
|
<div
|
|
134
187
|
class="tooltip"
|
|
135
188
|
class:visible
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
189
|
+
popover="manual"
|
|
190
|
+
{@attach measure}
|
|
191
|
+
{@attach popover}
|
|
192
|
+
ontransitionend={handleTransitionEnd}
|
|
139
193
|
style:translate
|
|
140
194
|
style:transform-origin={transformOrigin}
|
|
141
195
|
style:min-width="{minWidth}px"
|
|
@@ -154,6 +208,12 @@
|
|
|
154
208
|
</span>
|
|
155
209
|
</div>
|
|
156
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}
|
|
157
217
|
<div class="dots" class:show={isMulti}>
|
|
158
218
|
{#each matched as m, i (i)}
|
|
159
219
|
<span
|
|
@@ -167,24 +227,53 @@
|
|
|
167
227
|
|
|
168
228
|
<style>
|
|
169
229
|
.tooltip {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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)));
|
|
173
245
|
padding: 6px 10px;
|
|
174
246
|
border-radius: 4px;
|
|
175
247
|
font-family: var(--fjader-font, system-ui, sans-serif);
|
|
176
248
|
font-size: 12px;
|
|
177
249
|
line-height: 1.4;
|
|
178
250
|
pointer-events: none;
|
|
179
|
-
z-index: 1000;
|
|
180
251
|
white-space: nowrap;
|
|
181
|
-
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);
|
|
182
255
|
opacity: 0;
|
|
183
256
|
scale: 0.78;
|
|
184
257
|
transition:
|
|
185
258
|
opacity 100ms ease-out,
|
|
186
259
|
scale 200ms cubic-bezier(0.34, 1.3, 0.64, 1);
|
|
187
|
-
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));
|
|
188
277
|
}
|
|
189
278
|
|
|
190
279
|
.tooltip.visible {
|
|
@@ -192,6 +281,15 @@
|
|
|
192
281
|
scale: 1;
|
|
193
282
|
}
|
|
194
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
|
+
|
|
195
293
|
.row {
|
|
196
294
|
display: flex;
|
|
197
295
|
gap: 8px;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { OtherRowStats } from '../components/marks/lifecycle/other-rows.svelte';
|
|
1
2
|
import type { QueryEngine } from '../query-engine/QueryEngine';
|
|
2
3
|
import type { D3Scale, ScaleStateInstance } from '../types';
|
|
3
4
|
import type { DimValue } from '../query-engine/categorical-identity';
|
|
@@ -50,6 +51,11 @@ interface TooltipMarkRegistration {
|
|
|
50
51
|
};
|
|
51
52
|
constantRadius?: number;
|
|
52
53
|
displayFields?: string[] | null;
|
|
54
|
+
/**
|
|
55
|
+
* What an Other bar's point holds, for the tooltip to add its rows; read at
|
|
56
|
+
* display time, so a selection that changes under the pointer shows at once.
|
|
57
|
+
*/
|
|
58
|
+
otherStats?: (point: DataRow) => OtherRowStats | null;
|
|
53
59
|
proximityAxis: 'x' | 'y' | 'xy';
|
|
54
60
|
enabled: boolean;
|
|
55
61
|
}
|
|
@@ -83,11 +89,13 @@ export declare class TooltipManager {
|
|
|
83
89
|
currentChannelFields: Record<string, string> | null;
|
|
84
90
|
currentDisplayFields: string[] | null;
|
|
85
91
|
visible: boolean;
|
|
86
|
-
|
|
87
|
-
|
|
92
|
+
pointerClientX: number;
|
|
93
|
+
pointerClientY: number;
|
|
88
94
|
matchedRadius: number | null;
|
|
89
95
|
get focusedPoint(): DataRow | null;
|
|
90
96
|
get focusedMatch(): MatchedPoint | null;
|
|
97
|
+
/** The focused point's Other numbers, or null where it is not an Other bar. */
|
|
98
|
+
get focusedOtherStats(): OtherRowStats | null;
|
|
91
99
|
get currentPoint(): DataRow | null;
|
|
92
100
|
get currentColor(): string | null;
|
|
93
101
|
get matchedScreenX(): number | null;
|
|
@@ -97,6 +105,12 @@ export declare class TooltipManager {
|
|
|
97
105
|
* Update the cursor's data-space position. Called directly from pointer move handler.
|
|
98
106
|
*/
|
|
99
107
|
updatePosition(xData: number | null, yData: number | null): void;
|
|
108
|
+
/**
|
|
109
|
+
* Run the hover query again at the last position, if the pointer is still
|
|
110
|
+
* in the chart: a mark's tooltip table was rewritten under a still pointer,
|
|
111
|
+
* so the row it shows is the old data's. The next frame runs it.
|
|
112
|
+
*/
|
|
113
|
+
refresh(): void;
|
|
100
114
|
/**
|
|
101
115
|
* Cycle the focused point index by delta. Wraps around.
|
|
102
116
|
* Remembers the focused category so it persists across cursor moves.
|
|
@@ -121,10 +135,11 @@ export declare class TooltipManager {
|
|
|
121
135
|
*/
|
|
122
136
|
get currentMarkLOD(): MarkLODSurface | null;
|
|
123
137
|
/**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
138
|
+
* The pointer's viewport (client) position, where the tooltip is placed: it
|
|
139
|
+
* renders in the top layer, outside the chart's clipping ancestors. Called from
|
|
140
|
+
* the chart's pointer move handler.
|
|
126
141
|
*/
|
|
127
|
-
|
|
142
|
+
updatePointerClient(x: number, y: number): void;
|
|
128
143
|
/**
|
|
129
144
|
* Called once per animation frame by GPUCoordinator's render loop.
|
|
130
145
|
*/
|