@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
|
@@ -14,7 +14,16 @@ import type { ContinuousScaleState } from '../../../types';
|
|
|
14
14
|
export declare function fetchCategoricalDomains(coordinator: QueryEngine, table: string, select: Record<string, unknown>, actualXScaleType: string, actualYScaleType: string, xScaleState: BandScaleState | null, yScaleState: BandScaleState | null, facetBandScale: BandScaleState | null | undefined, fx: unknown, colorChannel: string, colorScaleState: ColorScaleState | null, sizeChannel: string, sizeScaleState: SizeScaleState | null, seriesScaleState: ColorScaleState | null, seriesChannel: string | null, bandDomainFromData: boolean, markId: string, explicitDomain?: {
|
|
15
15
|
x?: DimValue[];
|
|
16
16
|
y?: DimValue[];
|
|
17
|
-
} | null
|
|
17
|
+
} | null,
|
|
18
|
+
/** nulls="drop": the band and facet domains leave out the NULL category. */
|
|
19
|
+
dropNull?: boolean,
|
|
20
|
+
/**
|
|
21
|
+
* True once the setup run that called this has been superseded or its mark
|
|
22
|
+
* destroyed. Checked at every write, after each query returns: the scales
|
|
23
|
+
* it would write may have been dropped with a ChartScales the chart has
|
|
24
|
+
* since replaced.
|
|
25
|
+
*/
|
|
26
|
+
stale?: () => boolean): Promise<number>;
|
|
18
27
|
/**
|
|
19
28
|
* Fetch global min/max extents for aggregated fields using a "sidecar" query.
|
|
20
29
|
* Runs the same aggregation as the main query but without filters, wrapped
|
|
@@ -23,6 +32,8 @@ export declare function fetchCategoricalDomains(coordinator: QueryEngine, table:
|
|
|
23
32
|
* Returns `{ globalAggregatesSet: true }` on success so the caller can
|
|
24
33
|
* update its reactive state.
|
|
25
34
|
*/
|
|
26
|
-
export declare function fetchGlobalAggregates(coordinator: QueryEngine, table: string, select: Record<string, unknown>, channelsNeedingDataDomain: Set<string>, autoBinNodeCount: number, buildQueryFn: (table: string, config: unknown, filter: unknown[], forMosaic: boolean) => Query, config: unknown, xScaleState: ContinuousScaleState | null, yScaleState: ContinuousScaleState | null, colorScaleState: ColorScaleState | null, markId: string
|
|
35
|
+
export declare function fetchGlobalAggregates(coordinator: QueryEngine, table: string, select: Record<string, unknown>, channelsNeedingDataDomain: Set<string>, autoBinNodeCount: number, buildQueryFn: (table: string, config: unknown, filter: unknown[], forMosaic: boolean) => Query, config: unknown, xScaleState: ContinuousScaleState | null, yScaleState: ContinuousScaleState | null, colorScaleState: ColorScaleState | null, markId: string,
|
|
36
|
+
/** As for fetchCategoricalDomains: checked before writing the extents. */
|
|
37
|
+
stale?: () => boolean): Promise<{
|
|
27
38
|
globalAggregatesSet: boolean;
|
|
28
39
|
}>;
|
|
@@ -10,7 +10,20 @@ import { identityKeyFor } from '../../../query-engine/identity-key';
|
|
|
10
10
|
*
|
|
11
11
|
* Returns the number of fetches executed (for logging).
|
|
12
12
|
*/
|
|
13
|
-
export async function fetchCategoricalDomains(coordinator, table, select, actualXScaleType, actualYScaleType, xScaleState, yScaleState, facetBandScale, fx, colorChannel, colorScaleState, sizeChannel, sizeScaleState, seriesScaleState, seriesChannel, bandDomainFromData, markId, explicitDomain
|
|
13
|
+
export async function fetchCategoricalDomains(coordinator, table, select, actualXScaleType, actualYScaleType, xScaleState, yScaleState, facetBandScale, fx, colorChannel, colorScaleState, sizeChannel, sizeScaleState, seriesScaleState, seriesChannel, bandDomainFromData, markId, explicitDomain,
|
|
14
|
+
/** nulls="drop": the band and facet domains leave out the NULL category. */
|
|
15
|
+
dropNull = false,
|
|
16
|
+
/**
|
|
17
|
+
* True once the setup run that called this has been superseded or its mark
|
|
18
|
+
* destroyed. Checked at every write, after each query returns: the scales
|
|
19
|
+
* it would write may have been dropped with a ChartScales the chart has
|
|
20
|
+
* since replaced.
|
|
21
|
+
*/
|
|
22
|
+
stale = () => false) {
|
|
23
|
+
const categoriesOf = (key) => {
|
|
24
|
+
const categories = coordinator.categoryValues(key, table);
|
|
25
|
+
return dropNull ? categories.filter((c) => c !== null) : categories;
|
|
26
|
+
};
|
|
14
27
|
const fetches = [];
|
|
15
28
|
const setupFetch = (axis, field, scaleState) => {
|
|
16
29
|
if (!field || !scaleState)
|
|
@@ -64,8 +77,10 @@ export async function fetchCategoricalDomains(coordinator, table, select, actual
|
|
|
64
77
|
fetches.push((async () => {
|
|
65
78
|
// Idempotent and coalesced — the mark's own setup calls this too.
|
|
66
79
|
await coordinator.ensureColumns(table, { categorical: [identityField] });
|
|
80
|
+
if (stale())
|
|
81
|
+
return;
|
|
67
82
|
// This table's categories: the dim spans every table on the field.
|
|
68
|
-
const categories =
|
|
83
|
+
const categories = categoriesOf(identityField.key);
|
|
69
84
|
// A snapshot of the real categories. The scale adopts the live
|
|
70
85
|
// identity itself in MarkLifecycleV2's #ensureIdentityColumns, which
|
|
71
86
|
// also covers the explicit-domain axes that never reach this branch.
|
|
@@ -103,9 +118,8 @@ export async function fetchCategoricalDomains(coordinator, table, select, actual
|
|
|
103
118
|
// Registered from the data.
|
|
104
119
|
}
|
|
105
120
|
else if (explicitDomain?.y && yScaleState) {
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
deferredExplicitY = [...explicitDomain.y].reverse();
|
|
121
|
+
// Slot 0 is at the top of a y band axis, so the list is used as given.
|
|
122
|
+
deferredExplicitY = explicitDomain.y;
|
|
109
123
|
}
|
|
110
124
|
else {
|
|
111
125
|
setupFetch('y', select.y, yScaleState);
|
|
@@ -123,7 +137,9 @@ export async function fetchCategoricalDomains(coordinator, table, select, actual
|
|
|
123
137
|
fetches.push((async () => {
|
|
124
138
|
// Idempotent and coalesced — the mark's own setup calls this too.
|
|
125
139
|
await coordinator.ensureColumns(table, { categorical: [facetKey] });
|
|
126
|
-
|
|
140
|
+
if (stale())
|
|
141
|
+
return;
|
|
142
|
+
const categories = categoriesOf(facetKey.key);
|
|
127
143
|
if (categories.length > 0)
|
|
128
144
|
facetBandScale.addMark(categories, markId);
|
|
129
145
|
})());
|
|
@@ -145,7 +161,9 @@ export async function fetchCategoricalDomains(coordinator, table, select, actual
|
|
|
145
161
|
min_time: min(colExpr),
|
|
146
162
|
max_time: max(colExpr)
|
|
147
163
|
});
|
|
148
|
-
fetches.push(coordinator.query(q).then((result) => {
|
|
164
|
+
fetches.push(coordinator.query(q, { cache: true, source: table }).then((result) => {
|
|
165
|
+
if (stale())
|
|
166
|
+
return;
|
|
149
167
|
const row = Array.from(result)[0];
|
|
150
168
|
if (row && row.min_time != null && row.max_time != null) {
|
|
151
169
|
console.log(`[fetchCategoricalDomains] Time extent for ${axis}:`, [
|
|
@@ -187,6 +205,8 @@ export async function fetchCategoricalDomains(coordinator, table, select, actual
|
|
|
187
205
|
fetches.push((async () => {
|
|
188
206
|
// Idempotent and coalesced — the mark's own setup calls this too.
|
|
189
207
|
await coordinator.ensureColumns(table, { categorical: [colorIdentity] });
|
|
208
|
+
if (stale())
|
|
209
|
+
return;
|
|
190
210
|
const categories = coordinator.categoryValues(colorIdentity.key);
|
|
191
211
|
if (categories.length > 0) {
|
|
192
212
|
colorScaleState.setDomain(categories);
|
|
@@ -200,7 +220,9 @@ export async function fetchCategoricalDomains(coordinator, table, select, actual
|
|
|
200
220
|
min_color: cast(min(colExpr), 'REAL'),
|
|
201
221
|
max_color: cast(max(colExpr), 'REAL')
|
|
202
222
|
});
|
|
203
|
-
fetches.push(coordinator.query(q).then((result) => {
|
|
223
|
+
fetches.push(coordinator.query(q, { cache: true, source: table }).then((result) => {
|
|
224
|
+
if (stale())
|
|
225
|
+
return;
|
|
204
226
|
const row = Array.from(result)[0];
|
|
205
227
|
if (row && row.min_color != null && row.max_color != null) {
|
|
206
228
|
console.log(`[fetchCategoricalDomains] Color domain (sequential):`, [
|
|
@@ -221,7 +243,9 @@ export async function fetchCategoricalDomains(coordinator, table, select, actual
|
|
|
221
243
|
min_radius: cast(min(sqlColumn(String(sizeField))), 'REAL'),
|
|
222
244
|
max_radius: cast(max(sqlColumn(String(sizeField))), 'REAL')
|
|
223
245
|
});
|
|
224
|
-
fetches.push(coordinator.query(q).then((result) => {
|
|
246
|
+
fetches.push(coordinator.query(q, { cache: true, source: table }).then((result) => {
|
|
247
|
+
if (stale())
|
|
248
|
+
return;
|
|
225
249
|
const row = Array.from(result)[0];
|
|
226
250
|
if (row && row.min_radius != null && row.max_radius != null) {
|
|
227
251
|
sizeScaleState.setDomain([row.min_radius, row.max_radius]);
|
|
@@ -236,6 +260,8 @@ export async function fetchCategoricalDomains(coordinator, table, select, actual
|
|
|
236
260
|
if (seriesIdentity) {
|
|
237
261
|
fetches.push((async () => {
|
|
238
262
|
await coordinator.ensureColumns(table, { categorical: [seriesIdentity] });
|
|
263
|
+
if (stale())
|
|
264
|
+
return;
|
|
239
265
|
const categories = coordinator.categoryValues(seriesIdentity.key);
|
|
240
266
|
if (categories.length > 0) {
|
|
241
267
|
seriesScaleState.setDomain(categories);
|
|
@@ -245,6 +271,8 @@ export async function fetchCategoricalDomains(coordinator, table, select, actual
|
|
|
245
271
|
}
|
|
246
272
|
if (fetches.length > 0)
|
|
247
273
|
await Promise.all(fetches);
|
|
274
|
+
if (stale())
|
|
275
|
+
return fetches.length;
|
|
248
276
|
// Apply explicit domains after all async fetches — ViewManager columns
|
|
249
277
|
// from color/series scales are now registered, so the band view will
|
|
250
278
|
// include all necessary index columns.
|
|
@@ -267,7 +295,9 @@ export async function fetchCategoricalDomains(coordinator, table, select, actual
|
|
|
267
295
|
* Returns `{ globalAggregatesSet: true }` on success so the caller can
|
|
268
296
|
* update its reactive state.
|
|
269
297
|
*/
|
|
270
|
-
export async function fetchGlobalAggregates(coordinator, table, select, channelsNeedingDataDomain, autoBinNodeCount, buildQueryFn, config, xScaleState, yScaleState, colorScaleState, markId
|
|
298
|
+
export async function fetchGlobalAggregates(coordinator, table, select, channelsNeedingDataDomain, autoBinNodeCount, buildQueryFn, config, xScaleState, yScaleState, colorScaleState, markId,
|
|
299
|
+
/** As for fetchCategoricalDomains: checked before writing the extents. */
|
|
300
|
+
stale = () => false) {
|
|
271
301
|
// Only proceed if we have channels needing data-derived domains
|
|
272
302
|
if (channelsNeedingDataDomain.size === 0) {
|
|
273
303
|
console.log(`[${markId}] No aggregated channels, skipping sidecar query`);
|
|
@@ -329,7 +359,9 @@ export async function fetchGlobalAggregates(coordinator, table, select, channels
|
|
|
329
359
|
const sidecarQuery = Query.from(innerQuery).select(outerSelect);
|
|
330
360
|
console.log(`📊 [${markId}] Sidecar query SQL:`, sidecarQuery.toString());
|
|
331
361
|
// Step 3: Execute the query
|
|
332
|
-
const result = await coordinator.query(sidecarQuery);
|
|
362
|
+
const result = await coordinator.query(sidecarQuery, { cache: true, source: table });
|
|
363
|
+
if (stale())
|
|
364
|
+
return { globalAggregatesSet: false };
|
|
333
365
|
const extents = Array.from(result)[0];
|
|
334
366
|
console.log(`✅ [${markId}] Sidecar results:`, extents);
|
|
335
367
|
// Step 4: Apply extents to scales
|
|
@@ -39,5 +39,10 @@ export declare function setInitialContinuousDomains(fieldInfo: Record<string, Fi
|
|
|
39
39
|
export declare function registerDataDerivedDomains(data: DataAccessor, channelsNeedingDataDomain: Set<string>, additionalChannels: string[], skipDomainUpdateForAxis: string | null, zeroBaseline: {
|
|
40
40
|
x?: boolean;
|
|
41
41
|
y?: boolean;
|
|
42
|
-
} | undefined, actualXScaleType: string, actualYScaleType: string, xScaleState: ContinuousScaleState | null, yScaleState: ContinuousScaleState | null, bandDomainFromData: boolean, xBandScaleState: BandScaleState | null, yBandScaleState: BandScaleState | null, markId: string,
|
|
42
|
+
} | undefined, actualXScaleType: string, actualYScaleType: string, xScaleState: ContinuousScaleState | null, yScaleState: ContinuousScaleState | null, bandDomainFromData: boolean, xBandScaleState: BandScaleState | null, yBandScaleState: BandScaleState | null, markId: string,
|
|
43
|
+
/**
|
|
44
|
+
* Whether a scale's change from this result lands without animating: a
|
|
45
|
+
* catch-up result on a scale no other visible chart draws. Null: animate.
|
|
46
|
+
*/
|
|
47
|
+
instantFor: ((scale: object) => boolean) | null, colorScaleState?: ColorScaleState | null): RegisterDomainsResult;
|
|
43
48
|
export {};
|
|
@@ -100,7 +100,12 @@ export function setInitialContinuousDomains(fieldInfo, scaleMap, select, actualX
|
|
|
100
100
|
* Called from Effect 7 on each data update. Uses positional args to avoid
|
|
101
101
|
* object allocation on the crossfilter hot path.
|
|
102
102
|
*/
|
|
103
|
-
export function registerDataDerivedDomains(data, channelsNeedingDataDomain, additionalChannels, skipDomainUpdateForAxis, zeroBaseline, actualXScaleType, actualYScaleType, xScaleState, yScaleState, bandDomainFromData, xBandScaleState, yBandScaleState, markId,
|
|
103
|
+
export function registerDataDerivedDomains(data, channelsNeedingDataDomain, additionalChannels, skipDomainUpdateForAxis, zeroBaseline, actualXScaleType, actualYScaleType, xScaleState, yScaleState, bandDomainFromData, xBandScaleState, yBandScaleState, markId,
|
|
104
|
+
/**
|
|
105
|
+
* Whether a scale's change from this result lands without animating: a
|
|
106
|
+
* catch-up result on a scale no other visible chart draws. Null: animate.
|
|
107
|
+
*/
|
|
108
|
+
instantFor, colorScaleState) {
|
|
104
109
|
if (!data || data.length === 0) {
|
|
105
110
|
return { dataDerivedDomainsSet: false, skipDomainUpdateForAxis };
|
|
106
111
|
}
|
|
@@ -152,7 +157,8 @@ export function registerDataDerivedDomains(data, channelsNeedingDataDomain, addi
|
|
|
152
157
|
if (zeroBaseline?.x) {
|
|
153
158
|
extent = [Math.min(0, extent[0]), Math.max(0, extent[1])];
|
|
154
159
|
}
|
|
155
|
-
|
|
160
|
+
const instant = !!xScaleState && (instantFor?.(xScaleState) ?? false);
|
|
161
|
+
registerExtentOnScale(xScaleState, actualXScaleType, extent, markId, instant, 'X');
|
|
156
162
|
}
|
|
157
163
|
}
|
|
158
164
|
// Compute and register Y domain
|
|
@@ -166,7 +172,8 @@ export function registerDataDerivedDomains(data, channelsNeedingDataDomain, addi
|
|
|
166
172
|
if (zeroBaseline?.y) {
|
|
167
173
|
extent = [Math.min(0, extent[0]), Math.max(0, extent[1])];
|
|
168
174
|
}
|
|
169
|
-
|
|
175
|
+
const instant = !!yScaleState && (instantFor?.(yScaleState) ?? false);
|
|
176
|
+
registerExtentOnScale(yScaleState, actualYScaleType, extent, markId, instant, 'Y');
|
|
170
177
|
}
|
|
171
178
|
}
|
|
172
179
|
// Band scale domain from the query result, for a mark whose axis is decided
|
|
@@ -193,12 +200,8 @@ export function registerDataDerivedDomains(data, channelsNeedingDataDomain, addi
|
|
|
193
200
|
// order the ranked query had. See RANK_COLUMN.
|
|
194
201
|
const ranked = data.getColumn(RANK_COLUMN) != null;
|
|
195
202
|
const ids = extractIdsRankedFromData(data, axis, RANK_COLUMN);
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
// topN's and the sort's alike — rather than by whichever transform
|
|
199
|
-
// produced the rank.
|
|
200
|
-
if (ranked && axis === 'y')
|
|
201
|
-
ids.reverse();
|
|
203
|
+
// Used as ranked on both axes: slot 0 is at the left of x and the
|
|
204
|
+
// top of y, where a ranking is read from.
|
|
202
205
|
if (ids.length === 0)
|
|
203
206
|
continue;
|
|
204
207
|
// `sorted` says this list is a ranking the axis should prefer over
|
|
@@ -221,7 +224,7 @@ export function registerDataDerivedDomains(data, channelsNeedingDataDomain, addi
|
|
|
221
224
|
continue;
|
|
222
225
|
bandScale.addMark(categories, markId);
|
|
223
226
|
}
|
|
224
|
-
bandScale.updateDomainFromExtents();
|
|
227
|
+
bandScale.updateDomainFromExtents({ instant: instantFor?.(bandScale) ?? false });
|
|
225
228
|
}
|
|
226
229
|
}
|
|
227
230
|
// Color domain from query results (needed when sidecar is skipped, e.g. binned marks)
|
|
@@ -244,20 +247,14 @@ export function registerDataDerivedDomains(data, channelsNeedingDataDomain, addi
|
|
|
244
247
|
// ---------------------------------------------------------------------------
|
|
245
248
|
// Internal helper
|
|
246
249
|
// ---------------------------------------------------------------------------
|
|
247
|
-
function registerExtentOnScale(scaleState, scaleType, extent, markId,
|
|
250
|
+
function registerExtentOnScale(scaleState, scaleType, extent, markId, instant, axisLabel) {
|
|
248
251
|
if (scaleType === 'time' && scaleState?.addOffsetExtent) {
|
|
249
252
|
// Time scale: data is in offset form, use addOffsetExtent
|
|
250
253
|
console.log(`📊 [${markId}] Registering ${axisLabel} domain from data (time offsets):`, extent);
|
|
251
|
-
scaleState.addOffsetExtent(extent, markId);
|
|
254
|
+
scaleState.addOffsetExtent(extent, markId, { instant });
|
|
252
255
|
}
|
|
253
256
|
else if (scaleState?.addMark) {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
console.log(`⚡ [${markId}] Instant ${axisLabel} domain update (rebinning):`, extent);
|
|
257
|
-
}
|
|
258
|
-
else {
|
|
259
|
-
console.log(`📊 [${markId}] Registering ${axisLabel} domain from data:`, extent);
|
|
260
|
-
}
|
|
261
|
-
scaleState.addMark(extent, markId, { instant: isRebinning });
|
|
257
|
+
console.log(`📊 [${markId}] Registering ${axisLabel} domain from data:`, extent);
|
|
258
|
+
scaleState.addMark(extent, markId, { instant });
|
|
262
259
|
}
|
|
263
260
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a mark's Other rows hold, and what is done with it: the one owner of
|
|
3
|
+
* the three things that read an Other row's member lists.
|
|
4
|
+
*
|
|
5
|
+
* - `register`: hands the members to their field, so a click on the bucket
|
|
6
|
+
* selects them and the bucket is lit while the selection reaches into it.
|
|
7
|
+
* - `split`: a bar's selected share, for the bar renderer to draw as two parts.
|
|
8
|
+
* - `stats`: what that share is made of, for a tooltip.
|
|
9
|
+
*
|
|
10
|
+
* Takes getters, not values, so what it reads is tracked by whoever reads it.
|
|
11
|
+
* MarkLifecycleV2 holds one and says only what it is told by the mark.
|
|
12
|
+
*/
|
|
13
|
+
import type { Table } from '@uwdata/flechette';
|
|
14
|
+
import type { FilterGroup } from '../../../query-engine/FilterGroup.svelte';
|
|
15
|
+
import type { QueryEngine } from '../../../query-engine/QueryEngine';
|
|
16
|
+
import type { ChannelIdentity } from '../../../types';
|
|
17
|
+
/** What the mark is, for whether its Other bar can be split. */
|
|
18
|
+
export interface SplitPlan {
|
|
19
|
+
markType: string;
|
|
20
|
+
/** The bar's length channel: `y` for vertical bars, `x` for horizontal. */
|
|
21
|
+
length: string | undefined;
|
|
22
|
+
/** The measure topN ranks by, null for its default (the length). */
|
|
23
|
+
by: string | null;
|
|
24
|
+
/** The length channel's select expression. */
|
|
25
|
+
expr: unknown;
|
|
26
|
+
/** The mark's topN has an Other bucket. */
|
|
27
|
+
hasOther: boolean;
|
|
28
|
+
/** JS transforms follow the query, so its rows no longer line up. */
|
|
29
|
+
hasJsTransforms: boolean;
|
|
30
|
+
}
|
|
31
|
+
/** What the bar renderer draws an Other split from. */
|
|
32
|
+
export interface SplitProp {
|
|
33
|
+
/** 0 on rows that are not an Other row, `1 + share` on one. */
|
|
34
|
+
data: Float32Array;
|
|
35
|
+
/** Whether the split travels with the bar's morph or lands at once. */
|
|
36
|
+
slides: boolean;
|
|
37
|
+
/** The band axis the bars are grouped on; the split runs along the other. */
|
|
38
|
+
axis: 'x' | 'y';
|
|
39
|
+
/** Colour is the band's own field, so an Other row's colour term reads its id too. */
|
|
40
|
+
colorOnBand: boolean;
|
|
41
|
+
}
|
|
42
|
+
/** One Other row's share, in numbers. */
|
|
43
|
+
export interface OtherRowStats {
|
|
44
|
+
picked: number;
|
|
45
|
+
total: number;
|
|
46
|
+
pickedCount: number;
|
|
47
|
+
memberCount: number;
|
|
48
|
+
/** The field has a selection; without one picked is everything and says nothing. */
|
|
49
|
+
active: boolean;
|
|
50
|
+
}
|
|
51
|
+
export interface OtherRowsDeps {
|
|
52
|
+
queryEngine: QueryEngine;
|
|
53
|
+
filterGroup: FilterGroup;
|
|
54
|
+
markId: string;
|
|
55
|
+
/** The mark's result, and the version it is at. */
|
|
56
|
+
table: () => Table | null;
|
|
57
|
+
dataVersion: () => number;
|
|
58
|
+
identity: () => Record<string, ChannelIdentity> | null;
|
|
59
|
+
/** The identity key of a band axis, or null when it has none. */
|
|
60
|
+
identityKey: (axis: 'x' | 'y') => string | null;
|
|
61
|
+
/**
|
|
62
|
+
* The mark is filtered by every selection (`'filter'`): its Other is
|
|
63
|
+
* recomputed inside the selection, so a click on it drills down.
|
|
64
|
+
*/
|
|
65
|
+
filtered: () => boolean;
|
|
66
|
+
/**
|
|
67
|
+
* A selection can dim the mark (`'auto'`). Not under `'filter'` or
|
|
68
|
+
* `'ignore'`, whose Other bars get no selected share.
|
|
69
|
+
*/
|
|
70
|
+
dims: () => boolean;
|
|
71
|
+
/** The mark's plan, or null before its setup completes. */
|
|
72
|
+
plan: () => SplitPlan | null;
|
|
73
|
+
/** The identity key of the mark's ordinal colour scale, or null. */
|
|
74
|
+
colorKey: () => string | null;
|
|
75
|
+
/** The identity key of the chart's band scale on an axis, or null. */
|
|
76
|
+
bandScaleKey: (axis: 'x' | 'y') => string | null;
|
|
77
|
+
}
|
|
78
|
+
export declare class OtherRows {
|
|
79
|
+
#private;
|
|
80
|
+
constructor(deps: OtherRowsDeps);
|
|
81
|
+
/**
|
|
82
|
+
* Register this result's Other rows with their field: what each Other bucket
|
|
83
|
+
* holds, per facet cell, for a click on it to select (FieldActivity.setOther).
|
|
84
|
+
* Per result, so the members follow the filters the mark receives. A mark
|
|
85
|
+
* filtered by the selection (`'filter'`) registers with no group, which is
|
|
86
|
+
* how a click on its Other knows to drill down rather than toggle off.
|
|
87
|
+
*/
|
|
88
|
+
register(): void;
|
|
89
|
+
/** Give every registered row up: the mark is going away or starting over. */
|
|
90
|
+
unregister(): void;
|
|
91
|
+
/**
|
|
92
|
+
* The bar renderer's split, or null: no split, the bar is lit or dim as a
|
|
93
|
+
* whole (see #splitAxis for when a bar has one).
|
|
94
|
+
*
|
|
95
|
+
* Per row, share = Σ values[k] · selected(ids[k]) / Σ values[k] over the
|
|
96
|
+
* row's own member lists: the part of the bar the selection holds. A weighted
|
|
97
|
+
* sum over values the query already returned, in JS, per result and per
|
|
98
|
+
* selection change: the alternative is re-querying a mark that dims, which is
|
|
99
|
+
* what dimming exists to avoid.
|
|
100
|
+
*/
|
|
101
|
+
get split(): SplitProp | null;
|
|
102
|
+
/** What an Other row's share is made of, or null where it has no split. */
|
|
103
|
+
stats(row: number): OtherRowStats | null;
|
|
104
|
+
/**
|
|
105
|
+
* `stats` of the Other row a tooltip point names: its band id and, with
|
|
106
|
+
* facets, its cell (null: any). A tooltip's rows are not the result's, so it
|
|
107
|
+
* finds its row by identity. A scan of the result's rows, per hover change
|
|
108
|
+
* and not per frame; null where the point is not an Other row of this mark's
|
|
109
|
+
* split.
|
|
110
|
+
*/
|
|
111
|
+
statsAt(bandId: number, facetId: number | null): OtherRowStats | null;
|
|
112
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a mark's Other rows hold, and what is done with it: the one owner of
|
|
3
|
+
* the three things that read an Other row's member lists.
|
|
4
|
+
*
|
|
5
|
+
* - `register`: hands the members to their field, so a click on the bucket
|
|
6
|
+
* selects them and the bucket is lit while the selection reaches into it.
|
|
7
|
+
* - `split`: a bar's selected share, for the bar renderer to draw as two parts.
|
|
8
|
+
* - `stats`: what that share is made of, for a tooltip.
|
|
9
|
+
*
|
|
10
|
+
* Takes getters, not values, so what it reads is tracked by whoever reads it.
|
|
11
|
+
* MarkLifecycleV2 holds one and says only what it is told by the mark.
|
|
12
|
+
*/
|
|
13
|
+
import { OTHER_IDS_COLUMN, OTHER_VALUES_COLUMN } from '../../../transforms';
|
|
14
|
+
import { otherSplit, splittable } from './other-split';
|
|
15
|
+
export class OtherRows {
|
|
16
|
+
#deps;
|
|
17
|
+
/** The (field, Other id) pairs this mark has rows registered under. */
|
|
18
|
+
#registered = new Map();
|
|
19
|
+
/** The split last computed, and for which result: what a selection change holds against. */
|
|
20
|
+
#last = null;
|
|
21
|
+
constructor(deps) {
|
|
22
|
+
this.#deps = deps;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Register this result's Other rows with their field: what each Other bucket
|
|
26
|
+
* holds, per facet cell, for a click on it to select (FieldActivity.setOther).
|
|
27
|
+
* Per result, so the members follow the filters the mark receives. A mark
|
|
28
|
+
* filtered by the selection (`'filter'`) registers with no group, which is
|
|
29
|
+
* how a click on its Other knows to drill down rather than toggle off.
|
|
30
|
+
*/
|
|
31
|
+
register() {
|
|
32
|
+
const { table: getTable, identity: getIdentity, filterGroup, markId } = this.#deps;
|
|
33
|
+
const table = getTable();
|
|
34
|
+
const identity = getIdentity();
|
|
35
|
+
const group = this.#deps.filtered() ? null : filterGroup.index;
|
|
36
|
+
const now = new Map();
|
|
37
|
+
const ids = table?.getChild(OTHER_IDS_COLUMN) ?? null;
|
|
38
|
+
if (table && identity && ids) {
|
|
39
|
+
const facets = table.getChild('__facet');
|
|
40
|
+
for (const axis of ['x', 'y']) {
|
|
41
|
+
const otherId = identity[axis]?.otherId;
|
|
42
|
+
const key = this.#deps.identityKey(axis);
|
|
43
|
+
const band = table.getChild(axis);
|
|
44
|
+
if (otherId == null || !key || !band)
|
|
45
|
+
continue;
|
|
46
|
+
const rows = [];
|
|
47
|
+
for (let i = 0; i < table.numRows; i++) {
|
|
48
|
+
if (Number(band.at(i)) !== otherId)
|
|
49
|
+
continue;
|
|
50
|
+
const members = ids.at(i);
|
|
51
|
+
if (!members)
|
|
52
|
+
continue;
|
|
53
|
+
rows.push({ facetId: facets ? Number(facets.at(i)) : null, ids: members });
|
|
54
|
+
}
|
|
55
|
+
filterGroup.activity(key).setOther(otherId, markId, { group, rows });
|
|
56
|
+
now.set(key, otherId);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// A field or bucket this mark no longer draws an Other on gives its rows up.
|
|
60
|
+
for (const [key, otherId] of this.#registered) {
|
|
61
|
+
if (now.get(key) !== otherId)
|
|
62
|
+
filterGroup.activity(key).setOther(otherId, markId, null);
|
|
63
|
+
}
|
|
64
|
+
this.#registered = now;
|
|
65
|
+
}
|
|
66
|
+
/** Give every registered row up: the mark is going away or starting over. */
|
|
67
|
+
unregister() {
|
|
68
|
+
for (const [key, otherId] of this.#registered) {
|
|
69
|
+
this.#deps.filterGroup.activity(key).setOther(otherId, this.#deps.markId, null);
|
|
70
|
+
}
|
|
71
|
+
this.#registered = new Map();
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The bar renderer's split, or null: no split, the bar is lit or dim as a
|
|
75
|
+
* whole (see #splitAxis for when a bar has one).
|
|
76
|
+
*
|
|
77
|
+
* Per row, share = Σ values[k] · selected(ids[k]) / Σ values[k] over the
|
|
78
|
+
* row's own member lists: the part of the bar the selection holds. A weighted
|
|
79
|
+
* sum over values the query already returned, in JS, per result and per
|
|
80
|
+
* selection change: the alternative is re-querying a mark that dims, which is
|
|
81
|
+
* what dimming exists to avoid.
|
|
82
|
+
*/
|
|
83
|
+
get split() {
|
|
84
|
+
const axis = this.#splitAxis;
|
|
85
|
+
const computed = this.#computed;
|
|
86
|
+
if (!axis || !computed)
|
|
87
|
+
return null;
|
|
88
|
+
return {
|
|
89
|
+
data: computed.split,
|
|
90
|
+
slides: computed.slides,
|
|
91
|
+
axis,
|
|
92
|
+
colorOnBand: this.#colorOnBand(axis)
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/** What an Other row's share is made of, or null where it has no split. */
|
|
96
|
+
stats(row) {
|
|
97
|
+
const computed = this.#computed;
|
|
98
|
+
if (!computed || computed.memberCount[row] === 0)
|
|
99
|
+
return null;
|
|
100
|
+
return {
|
|
101
|
+
picked: computed.picked[row],
|
|
102
|
+
total: computed.total[row],
|
|
103
|
+
pickedCount: computed.pickedCount[row],
|
|
104
|
+
memberCount: computed.memberCount[row],
|
|
105
|
+
active: computed.active
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* `stats` of the Other row a tooltip point names: its band id and, with
|
|
110
|
+
* facets, its cell (null: any). A tooltip's rows are not the result's, so it
|
|
111
|
+
* finds its row by identity. A scan of the result's rows, per hover change
|
|
112
|
+
* and not per frame; null where the point is not an Other row of this mark's
|
|
113
|
+
* split.
|
|
114
|
+
*/
|
|
115
|
+
statsAt(bandId, facetId) {
|
|
116
|
+
const axis = this.#splitAxis;
|
|
117
|
+
const table = this.#deps.table();
|
|
118
|
+
const otherId = axis ? this.#deps.identity()?.[axis]?.otherId : null;
|
|
119
|
+
if (!axis || !table || otherId == null || bandId !== otherId)
|
|
120
|
+
return null;
|
|
121
|
+
const band = table.getChild(axis);
|
|
122
|
+
const facets = facetId == null ? null : table.getChild('__facet');
|
|
123
|
+
if (!band)
|
|
124
|
+
return null;
|
|
125
|
+
for (let row = 0; row < table.numRows; row++) {
|
|
126
|
+
if (Number(band.at(row)) !== bandId)
|
|
127
|
+
continue;
|
|
128
|
+
if (facets && Number(facets.at(row)) !== facetId)
|
|
129
|
+
continue;
|
|
130
|
+
return this.stats(row);
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The band axis whose Other bar splits, or null. Only where a share is what
|
|
136
|
+
* it looks like:
|
|
137
|
+
* - a bar mark ('bary', 'barx') with a topN Other bucket, dimming (not
|
|
138
|
+
* 'filter' or 'ignore', which never dim) and without JS transforms, whose rows would
|
|
139
|
+
* no longer line up with the query's;
|
|
140
|
+
* - whose length is the measure topN ranks by, which is what Other sums;
|
|
141
|
+
* - and that measure a bare COUNT or SUM, or a raw column: a mean or a max
|
|
142
|
+
* summed into Other is no total, and a share of it would mean nothing.
|
|
143
|
+
*/
|
|
144
|
+
get #splitAxis() {
|
|
145
|
+
const plan = this.#deps.plan();
|
|
146
|
+
if (!plan || !this.#deps.dims())
|
|
147
|
+
return null;
|
|
148
|
+
if (!plan.hasOther || plan.hasJsTransforms)
|
|
149
|
+
return null;
|
|
150
|
+
if (plan.markType !== 'bary' && plan.markType !== 'barx')
|
|
151
|
+
return null;
|
|
152
|
+
return splittable(plan.length, plan.by, plan.expr)
|
|
153
|
+
? plan.markType === 'barx'
|
|
154
|
+
? 'y'
|
|
155
|
+
: 'x'
|
|
156
|
+
: null;
|
|
157
|
+
}
|
|
158
|
+
/** Whether colour is the band's own field, so an Other row's colour term reads its id too. */
|
|
159
|
+
#colorOnBand(axis) {
|
|
160
|
+
const color = this.#deps.colorKey();
|
|
161
|
+
return color != null && color === this.#deps.bandScaleKey(axis);
|
|
162
|
+
}
|
|
163
|
+
#computed = $derived.by(() => {
|
|
164
|
+
const axis = this.#splitAxis;
|
|
165
|
+
if (!axis)
|
|
166
|
+
return null;
|
|
167
|
+
const dataVersion = this.#deps.dataVersion();
|
|
168
|
+
const table = this.#deps.table();
|
|
169
|
+
const otherId = this.#deps.identity()?.[axis]?.otherId;
|
|
170
|
+
const key = this.#deps.identityKey(axis);
|
|
171
|
+
const ids = table?.getChild(OTHER_IDS_COLUMN) ?? null;
|
|
172
|
+
const values = table?.getChild(OTHER_VALUES_COLUMN) ?? null;
|
|
173
|
+
const band = table?.getChild(axis) ?? null;
|
|
174
|
+
if (!table || otherId == null || !key || !ids || !values || !band)
|
|
175
|
+
return null;
|
|
176
|
+
// The last split, for the same result only: a selection change holds a
|
|
177
|
+
// flat row's position. A new result starts afresh and travels with the bar.
|
|
178
|
+
const { filterGroup, queryEngine } = this.#deps;
|
|
179
|
+
const last = this.#last;
|
|
180
|
+
const next = otherSplit({
|
|
181
|
+
rows: table.numRows,
|
|
182
|
+
band,
|
|
183
|
+
otherId,
|
|
184
|
+
ids,
|
|
185
|
+
values,
|
|
186
|
+
selected: filterGroup.activity(key).selectedValues(filterGroup.index),
|
|
187
|
+
decode: queryEngine.identityFor(key).values,
|
|
188
|
+
previous: last && last.dataVersion === dataVersion ? last.split : null
|
|
189
|
+
});
|
|
190
|
+
this.#last = { dataVersion, split: next };
|
|
191
|
+
return next;
|
|
192
|
+
});
|
|
193
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An Other bar's selected share, per row, and when a bar has one: the pure
|
|
3
|
+
* half of MarkLifecycleV2's `otherSplit`, so it can be tested without a mark.
|
|
4
|
+
*/
|
|
5
|
+
/** A column of a result, as Arrow (flechette) hands it: one value per row. */
|
|
6
|
+
export interface RowColumn {
|
|
7
|
+
at(row: number): unknown;
|
|
8
|
+
}
|
|
9
|
+
/** One computed split: the channel, and which Other rows are flat. */
|
|
10
|
+
export interface OtherSplit {
|
|
11
|
+
/** 0 on rows that are not an Other row, `1 + share` (or a held position) on one. */
|
|
12
|
+
split: Float32Array;
|
|
13
|
+
/**
|
|
14
|
+
* 1 on an Other row whose two parts look the same: nothing selected (both
|
|
15
|
+
* lit), or none of its members selected (both dim). Its split position is
|
|
16
|
+
* invisible there, so it is held, and moving out of it lands rather than
|
|
17
|
+
* slides.
|
|
18
|
+
*/
|
|
19
|
+
flat: Uint8Array;
|
|
20
|
+
/**
|
|
21
|
+
* Whether the split travels with this write (the renderer morphs it) or
|
|
22
|
+
* lands at once. It lands when a row leaves a flat state: both sides of it
|
|
23
|
+
* look the same at that moment, so a slide would be a movement the eye
|
|
24
|
+
* reads as meaning something; the fades show the change instead.
|
|
25
|
+
*/
|
|
26
|
+
slides: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* What the share is made of, per row (0 on a row with no split), for a
|
|
29
|
+
* tooltip to say: `picked` of `total` (Σ values over the selected members,
|
|
30
|
+
* over all), `pickedCount` of `memberCount` categories. With nothing
|
|
31
|
+
* selected everything counts as picked.
|
|
32
|
+
*/
|
|
33
|
+
picked: Float64Array;
|
|
34
|
+
total: Float64Array;
|
|
35
|
+
pickedCount: Int32Array;
|
|
36
|
+
memberCount: Int32Array;
|
|
37
|
+
/** The field has a selection: without one, everything counts as picked, which says nothing. */
|
|
38
|
+
active: boolean;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The split channel for the bar renderer, per row: share = Σ values · selected
|
|
42
|
+
* / Σ values over the row's own member lists. Selection is by value (`decode`
|
|
43
|
+
* maps an id to its value), so the NULL category needs nothing special. A row
|
|
44
|
+
* whose members mix signs gets no split (0): a share of a sum that cancels
|
|
45
|
+
* means nothing.
|
|
46
|
+
*
|
|
47
|
+
* `previous`: the last split for the SAME rows (the same result), when only the
|
|
48
|
+
* selection changed. A flat row then holds its previous position, so going flat
|
|
49
|
+
* (a selection cleared, or one that no longer reaches into Other) is a fade,
|
|
50
|
+
* not a slide. Without it (a new result) every row takes its value, and the
|
|
51
|
+
* split travels with the bar's own morph.
|
|
52
|
+
*
|
|
53
|
+
* Cost, measured: about 22 ns per Other member per call (the `selected.has`),
|
|
54
|
+
* so 0.1 ms at 5 000 members and about 1.2 ms at 50 000. Fine per click. If
|
|
55
|
+
* selection ever changes continuously (a brush on a band axis) over a very large
|
|
56
|
+
* Other, test membership against a Uint8Array indexed by id, built once per
|
|
57
|
+
* change, rather than a Set. The arrays allocated here are sized by rows.
|
|
58
|
+
*/
|
|
59
|
+
export declare function otherSplit(opts: {
|
|
60
|
+
rows: number;
|
|
61
|
+
band: RowColumn;
|
|
62
|
+
otherId: number;
|
|
63
|
+
ids: RowColumn;
|
|
64
|
+
values: RowColumn;
|
|
65
|
+
selected: ReadonlySet<unknown> | null;
|
|
66
|
+
decode: ArrayLike<unknown>;
|
|
67
|
+
previous?: OtherSplit | null;
|
|
68
|
+
}): OtherSplit;
|
|
69
|
+
/**
|
|
70
|
+
* Whether a bar's Other bucket can be split: its length must be what Other
|
|
71
|
+
* sums, and a total. `length` is the bar's length channel, `by` the channel
|
|
72
|
+
* topN ranks by (null: its default, the length), `expr` the length channel's
|
|
73
|
+
* select expression.
|
|
74
|
+
*
|
|
75
|
+
* Other sums `by`, so a length channel that is not `by` is 0 on the Other row.
|
|
76
|
+
* A bare COUNT or SUM, or a raw column (an ungrouped mark), is a total; a mean,
|
|
77
|
+
* a max, a ratio or a distinct count summed into Other is not, and a share of
|
|
78
|
+
* it would look exact while meaning nothing.
|
|
79
|
+
*/
|
|
80
|
+
export declare function splittable(length: string | undefined, by: string | null, expr: unknown): boolean;
|