@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
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* → setupComplete = true
|
|
12
12
|
*
|
|
13
13
|
* Registration: watches #resolvedQueryParts, registers with engine.
|
|
14
|
-
* Engine handles filter-driven re-queries via
|
|
14
|
+
* Engine handles filter-driven re-queries via onClauseChange() and
|
|
15
|
+
* onFilterChange().
|
|
15
16
|
*
|
|
16
17
|
* Params: watches sort/limit on ReactiveConfig.
|
|
17
18
|
* Changes trigger re-query for this mark only, no teardown.
|
|
@@ -19,7 +20,7 @@
|
|
|
19
20
|
import { untrack } from 'svelte';
|
|
20
21
|
import { Query, isNode, isAggregateExpression, isColumnRef, isColumnParam, sql, column as sqlColumn, cast, min, max, isNotNull, and } from '@uwdata/mosaic-sql';
|
|
21
22
|
import { TransformCompiler } from '../../../transforms/TransformCompiler';
|
|
22
|
-
import { withDenseCategories, withRanking } from '../../../transforms';
|
|
23
|
+
import { withDenseCategories, withoutOtherMembers, withRanking } from '../../../transforms';
|
|
23
24
|
import { ArrowDataAccessor } from '../../../data/DataAccessor';
|
|
24
25
|
import { MarkHandle } from '../../../query-engine/MarkHandle.svelte';
|
|
25
26
|
import { isContinuousScale, isNumericScale } from '../../../scales/scale-categories';
|
|
@@ -28,6 +29,7 @@ import { isBinExpression } from '../../../sql/bin';
|
|
|
28
29
|
import { isRollingAvgExpression } from '../../../sql/rolling-avg';
|
|
29
30
|
import { isWindowExpression } from '../../../sql/expression-utils';
|
|
30
31
|
import { identityKeyFor } from '../../../query-engine/identity-key';
|
|
32
|
+
import { activityReads, groupIndexForResponse, NO_READS, readsFor, readsForResponse, rendererForMark } from '../../rendering/activity-reads';
|
|
31
33
|
import { AdaptiveBinRung } from '../../../lod/AdaptiveBinRung.svelte';
|
|
32
34
|
/** A SQL identifier, quoted, with any double quote in it doubled. */
|
|
33
35
|
function ident(name) {
|
|
@@ -39,6 +41,8 @@ import { detectChannelsNeedingDataDomain } from './domain-computation';
|
|
|
39
41
|
import { buildQueryParts, assembleQuery, castReal } from './query-builder';
|
|
40
42
|
import { setScalesReady, markDeferredScalesReady, setInitialContinuousDomains, registerDataDerivedDomains } from './domain-registration';
|
|
41
43
|
import { fetchCategoricalDomains, fetchGlobalAggregates } from './domain-queries';
|
|
44
|
+
import { transformScaleKey } from './transform-scale-key';
|
|
45
|
+
import { OtherRows } from './other-rows.svelte';
|
|
42
46
|
// ---------------------------------------------------------------------------
|
|
43
47
|
// MarkLifecycleV2
|
|
44
48
|
// ---------------------------------------------------------------------------
|
|
@@ -182,8 +186,68 @@ export class MarkLifecycleV2 {
|
|
|
182
186
|
this.#inlineIdentityColumns(resolvedSelect);
|
|
183
187
|
this.#inlineTimeOffsets(resolvedSelect);
|
|
184
188
|
const parts = buildQueryParts(resolvedSelect, this.#config._orderConfig ?? null, this.#transformCompiler.hasSQLTransforms(), passthroughChannels);
|
|
189
|
+
const nullDrops = this.#nullDrops(semanticSelect, resolvedSelect);
|
|
190
|
+
if (nullDrops.length > 0)
|
|
191
|
+
parts.nullDrops = nullDrops;
|
|
185
192
|
return parts;
|
|
186
193
|
});
|
|
194
|
+
/**
|
|
195
|
+
* The mark's categorical channels, each with the source expression it draws:
|
|
196
|
+
* a band or point axis, an ordinal colour, a separate series, and the facet
|
|
197
|
+
* (`__facet`, from the chart's fx). What `nulls="drop"` filters.
|
|
198
|
+
*/
|
|
199
|
+
#categoricalChannels(select) {
|
|
200
|
+
const out = [];
|
|
201
|
+
const categorical = (type) => type === 'band' || type === 'point';
|
|
202
|
+
if (categorical(this.actualXScaleType))
|
|
203
|
+
out.push(['x', select.x]);
|
|
204
|
+
if (categorical(this.actualYScaleType))
|
|
205
|
+
out.push(['y', select.y]);
|
|
206
|
+
const colorChannel = this.#config.colorChannel || 'fill';
|
|
207
|
+
if (this.#colorScaleState?.isOrdinal)
|
|
208
|
+
out.push([colorChannel, select[colorChannel]]);
|
|
209
|
+
const seriesChannel = this.#config._orderConfig?.seriesChannel ?? null;
|
|
210
|
+
if (seriesChannel && seriesChannel !== colorChannel) {
|
|
211
|
+
out.push([seriesChannel, select[seriesChannel]]);
|
|
212
|
+
}
|
|
213
|
+
if (this.#chartContext.fx)
|
|
214
|
+
out.push(['__facet', this.#chartContext.fx]);
|
|
215
|
+
// Only row-level values: an aggregate has no NULL category to drop.
|
|
216
|
+
return out.filter(([, source]) => source != null &&
|
|
217
|
+
typeof source !== 'number' &&
|
|
218
|
+
!(isNode(source) && isAggregateExpression(source)));
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* `nulls="drop"`, by channel (see QueryParts.nullDrops). A channel the query
|
|
222
|
+
* selects as its dim's id column drops the NULL category's id; one still
|
|
223
|
+
* carrying its raw value (identity not landed yet) drops SQL NULL. Read off
|
|
224
|
+
* the select itself, so the two cannot disagree: inlining an id column is
|
|
225
|
+
* what changes the channel's expression.
|
|
226
|
+
*/
|
|
227
|
+
#nullDrops(semantic, resolved) {
|
|
228
|
+
if (this.#reactiveConfig.nulls !== 'drop')
|
|
229
|
+
return [];
|
|
230
|
+
const drops = [];
|
|
231
|
+
for (const [channel, source] of this.#categoricalChannels(semantic)) {
|
|
232
|
+
const encoded = channel === '__facet' ? '__facet' in resolved : resolved[channel] !== source;
|
|
233
|
+
if (channel === '__facet' && !encoded)
|
|
234
|
+
continue;
|
|
235
|
+
const key = encoded ? identityKeyFor(source)?.key : null;
|
|
236
|
+
const nullId = key ? this.#queryEngine.identityFor(key).nullId : null;
|
|
237
|
+
drops.push({ channel, nullId });
|
|
238
|
+
}
|
|
239
|
+
return drops;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* `nulls="drop"` for a query over the RAW select (no id columns, no
|
|
243
|
+
* `__facet`): `<source> IS NOT NULL` per categorical channel, the facet's
|
|
244
|
+
* on the chart's fx. Passed as that query's filter.
|
|
245
|
+
*/
|
|
246
|
+
#rawNullFilters(select) {
|
|
247
|
+
if (this.#reactiveConfig.nulls !== 'drop')
|
|
248
|
+
return [];
|
|
249
|
+
return this.#categoricalChannels(select).map(([, source]) => isNotNull((typeof source === 'string' ? sqlColumn(source) : source)));
|
|
250
|
+
}
|
|
187
251
|
// --- Domain State ---
|
|
188
252
|
#channelsNeedingDataDomain = new Set();
|
|
189
253
|
#dataDerivedDomainsSet = $state(false);
|
|
@@ -254,6 +318,25 @@ export class MarkLifecycleV2 {
|
|
|
254
318
|
// than rebuilding. With no sort it passes the query through.
|
|
255
319
|
this.#transformCompiler = new TransformCompiler(withRanking(withDenseCategories(config.transforms || [], config.dense !== false), () => this.#rankingSort), this.#transformConfig);
|
|
256
320
|
this.#detectAutoBins();
|
|
321
|
+
this.otherRows = new OtherRows({
|
|
322
|
+
queryEngine,
|
|
323
|
+
filterGroup,
|
|
324
|
+
markId,
|
|
325
|
+
table: () => this.#handle?.arrowTable ?? null,
|
|
326
|
+
dataVersion: () => this.dataVersion,
|
|
327
|
+
identity: () => this.#identity,
|
|
328
|
+
identityKey: (axis) => this.#identityKey(axis)?.key ?? null,
|
|
329
|
+
filtered: () => this.#reactiveConfig.selectionResponse === 'filter',
|
|
330
|
+
plan: () => this.#splitPlan,
|
|
331
|
+
colorKey: () => {
|
|
332
|
+
const color = this.#colorScaleState;
|
|
333
|
+
return color?.isOrdinal ? color.identityKey : null;
|
|
334
|
+
},
|
|
335
|
+
bandScaleKey: (axis) => {
|
|
336
|
+
const band = (axis === 'x' ? this.#chartContext.xScaleState : this.#chartContext.yScaleState);
|
|
337
|
+
return band?.identityKey ?? null;
|
|
338
|
+
}
|
|
339
|
+
});
|
|
257
340
|
// AdaptiveBinRung for binned marks (watches viewport domain, triggers rebin)
|
|
258
341
|
if (this.#autoBinNodes.size > 0 && options?.gpuCoordinator) {
|
|
259
342
|
const binAxes = [...this.#autoBinNodes.keys()].map((ch) => (ch.startsWith('x') ? 'x' : 'y'));
|
|
@@ -338,6 +421,11 @@ export class MarkLifecycleV2 {
|
|
|
338
421
|
if (this.#destroyed)
|
|
339
422
|
return;
|
|
340
423
|
this.#identityFields.clear();
|
|
424
|
+
// The Other ids name rows of the dropped dims. Every holder lets go, so
|
|
425
|
+
// the scale acquires afresh when setup re-runs; so do the registered rows.
|
|
426
|
+
this.otherRows.unregister();
|
|
427
|
+
for (const scale of [...this.#otherScales])
|
|
428
|
+
this.#releaseOther(scale);
|
|
341
429
|
// Every dim it names just went away — topN must not bucket into one.
|
|
342
430
|
this.#setIdentity(null);
|
|
343
431
|
this.setupComplete = false;
|
|
@@ -359,8 +447,12 @@ export class MarkLifecycleV2 {
|
|
|
359
447
|
let config = scaleRequirements.x.config || {};
|
|
360
448
|
if (scaleRequirements.x.type === 'band' && hasCatReducing) {
|
|
361
449
|
const key = this.#transformCompiler.getCategoryTransformKey();
|
|
362
|
-
if (key)
|
|
363
|
-
config = {
|
|
450
|
+
if (key) {
|
|
451
|
+
config = {
|
|
452
|
+
...config,
|
|
453
|
+
transformKey: transformScaleKey(table, key, this.#chartContext.config)
|
|
454
|
+
};
|
|
455
|
+
}
|
|
364
456
|
}
|
|
365
457
|
this.#chartContext.registerScaleType('x', scaleRequirements.x.type, scaleRequirements.x.priority, this.#markId, config, inferredScaleColumns?.x);
|
|
366
458
|
}
|
|
@@ -368,8 +460,12 @@ export class MarkLifecycleV2 {
|
|
|
368
460
|
let config = scaleRequirements.y.config || {};
|
|
369
461
|
if (scaleRequirements.y.type === 'band' && hasCatReducing) {
|
|
370
462
|
const key = this.#transformCompiler.getCategoryTransformKey();
|
|
371
|
-
if (key)
|
|
372
|
-
config = {
|
|
463
|
+
if (key) {
|
|
464
|
+
config = {
|
|
465
|
+
...config,
|
|
466
|
+
transformKey: transformScaleKey(table, key, this.#chartContext.config)
|
|
467
|
+
};
|
|
468
|
+
}
|
|
373
469
|
}
|
|
374
470
|
this.#chartContext.registerScaleType('y', scaleRequirements.y.type, scaleRequirements.y.priority, this.#markId, config, inferredScaleColumns?.y);
|
|
375
471
|
}
|
|
@@ -421,7 +517,14 @@ export class MarkLifecycleV2 {
|
|
|
421
517
|
const schema = this.#schema;
|
|
422
518
|
if (!schema)
|
|
423
519
|
return;
|
|
520
|
+
// Read after setup, once the scales have adopted identity. A change of
|
|
521
|
+
// reads or of selectionResponse re-registers the mark: a selection
|
|
522
|
+
// that dimmed it may now filter it. The key is a string, so reads
|
|
523
|
+
// rebuilt with the same contents do not.
|
|
524
|
+
void this.#readsKey;
|
|
525
|
+
const response = this.#reactiveConfig.selectionResponse;
|
|
424
526
|
untrack(() => {
|
|
527
|
+
this.#route = { reads: this.#reads, chart: this.#chartContext.config, response };
|
|
425
528
|
// registerMark handles cleanup of previous registration for this
|
|
426
529
|
// markId and reuses the existing handle (preserves arrowTable
|
|
427
530
|
// during re-query so the chart doesn't flash empty on rebin).
|
|
@@ -430,7 +533,8 @@ export class MarkLifecycleV2 {
|
|
|
430
533
|
baseTable: this.#config.table,
|
|
431
534
|
parts,
|
|
432
535
|
schema,
|
|
433
|
-
transformCompiler: this.#transformCompiler
|
|
536
|
+
transformCompiler: this.#transformCompiler,
|
|
537
|
+
route: this.#route
|
|
434
538
|
}, this.#filterGroup);
|
|
435
539
|
});
|
|
436
540
|
});
|
|
@@ -468,14 +572,35 @@ export class MarkLifecycleV2 {
|
|
|
468
572
|
return;
|
|
469
573
|
untrack(() => {
|
|
470
574
|
this.#handleDataDomains(dv);
|
|
575
|
+
this.otherRows.register();
|
|
471
576
|
});
|
|
472
577
|
});
|
|
473
|
-
// Tooltip:
|
|
578
|
+
// Tooltip: two effects, so a data update does not take the table away.
|
|
579
|
+
//
|
|
580
|
+
// This one owns the table's lifetime: it drops it when tooltips go from
|
|
581
|
+
// wanted to not. It used to be one effect with the write below, and its
|
|
582
|
+
// cleanup then ran on every data update too: the table was dropped and
|
|
583
|
+
// marked not ready, the mark's tooltip unregistered, and the hovered row
|
|
584
|
+
// went with it. A click right after a data update found no row under the
|
|
585
|
+
// pointer, read as a click on empty space and cleared the selection, and
|
|
586
|
+
// the next click did nothing until the pointer moved.
|
|
474
587
|
$effect(() => {
|
|
475
588
|
const tooltipConfig = this.#reactiveConfig.tooltip;
|
|
476
589
|
const fitDomain = this.#reactiveConfig.fitDomain;
|
|
477
|
-
|
|
478
|
-
|
|
590
|
+
if (tooltipConfig === false && !fitDomain)
|
|
591
|
+
return;
|
|
592
|
+
return () => {
|
|
593
|
+
clearTimeout(this.#tooltipWriteTimeout);
|
|
594
|
+
this.#tooltipTableReady = false;
|
|
595
|
+
this.#queryEngine.exec(`DROP TABLE IF EXISTS "${this.#tooltipTableName}"`).catch(() => { });
|
|
596
|
+
};
|
|
597
|
+
});
|
|
598
|
+
// And this one rewrites it when data updates (debounced). The old table
|
|
599
|
+
// stays readable until the rewrite replaces it.
|
|
600
|
+
$effect(() => {
|
|
601
|
+
const tooltipConfig = this.#reactiveConfig.tooltip;
|
|
602
|
+
const fitDomain = this.#reactiveConfig.fitDomain;
|
|
603
|
+
if (tooltipConfig === false && !fitDomain)
|
|
479
604
|
return;
|
|
480
605
|
const dataVersion = this.dataVersion;
|
|
481
606
|
if (dataVersion === 0)
|
|
@@ -488,12 +613,7 @@ export class MarkLifecycleV2 {
|
|
|
488
613
|
this.#writeTooltipTable(data);
|
|
489
614
|
}, 300);
|
|
490
615
|
});
|
|
491
|
-
|
|
492
|
-
return () => {
|
|
493
|
-
clearTimeout(this.#tooltipWriteTimeout);
|
|
494
|
-
this.#tooltipTableReady = false;
|
|
495
|
-
this.#queryEngine.exec(`DROP TABLE IF EXISTS "${this.#tooltipTableName}"`).catch(() => { });
|
|
496
|
-
};
|
|
616
|
+
return () => clearTimeout(this.#tooltipWriteTimeout);
|
|
497
617
|
});
|
|
498
618
|
// fitDomain — query DuckDB for fitted axis extent on driving axis change.
|
|
499
619
|
$effect(() => {
|
|
@@ -589,7 +709,7 @@ export class MarkLifecycleV2 {
|
|
|
589
709
|
const [schemaColumns, fetchCount, aggResult, fieldStats] = await Promise.all([
|
|
590
710
|
// Fetch table schema (cached on QueryEngine, needed for CAST type decisions)
|
|
591
711
|
this.#queryEngine.describe(this.#config.table),
|
|
592
|
-
fetchCategoricalDomains(this.#queryEngine, this.#config.table, this.#config.select, this.actualXScaleType, this.actualYScaleType, this.#
|
|
712
|
+
fetchCategoricalDomains(this.#queryEngine, this.#config.table, this.#config.select, this.actualXScaleType, this.actualYScaleType, this.#bandScaleForDomain('x'), this.#bandScaleForDomain('y'), this.#chartContext.facetBandScale, this.#chartContext.fx, this.#config.colorChannel || 'fill', this.#colorScaleState, this.#config.sizeChannel || 'r', this.#sizeScaleState, this.#seriesScaleState, seriesKey, this.#bandDomainFromData, this.#markId, this.#reactiveConfig.domain, this.#reactiveConfig.nulls === 'drop'),
|
|
593
713
|
fetchGlobalAggregates(this.#queryEngine, this.#config.table, this.#config.select, this.#channelsNeedingDataDomain, this.#autoBinNodes.size, this.#buildQueryForAggregates.bind(this), this.#config, this.#chartContext.xScaleState, this.#chartContext.yScaleState, this.#colorScaleState, this.#markId),
|
|
594
714
|
this.#fetchFieldStats(),
|
|
595
715
|
this.#ensureIdentityColumns()
|
|
@@ -849,7 +969,7 @@ export class MarkLifecycleV2 {
|
|
|
849
969
|
const additionalChannels = this.#transformCompiler.getAdditionalChannels();
|
|
850
970
|
const xState = this.#chartContext.xScaleState;
|
|
851
971
|
const yState = this.#chartContext.yScaleState;
|
|
852
|
-
const domainResult = registerDataDerivedDomains(data, this.#channelsNeedingDataDomain, additionalChannels, this.#skipDomainUpdateForAxis, this.#config.zeroBaseline, this.actualXScaleType, this.actualYScaleType, xState, yState, this.#bandDomainFromData, this.#
|
|
972
|
+
const domainResult = registerDataDerivedDomains(data, this.#channelsNeedingDataDomain, additionalChannels, this.#skipDomainUpdateForAxis, this.#config.zeroBaseline, this.actualXScaleType, this.actualYScaleType, xState, yState, this.#bandDomainFromData, this.#bandScaleForDomain('x'), this.#bandScaleForDomain('y'), this.#markId, false, // isRebinning
|
|
853
973
|
this.#colorScaleState);
|
|
854
974
|
this.#dataDerivedDomainsSet = domainResult.dataDerivedDomainsSet;
|
|
855
975
|
this.#skipDomainUpdateForAxis = domainResult.skipDomainUpdateForAxis;
|
|
@@ -983,10 +1103,16 @@ export class MarkLifecycleV2 {
|
|
|
983
1103
|
// one id, which does not move a row limit that coarse.
|
|
984
1104
|
const identity = {};
|
|
985
1105
|
const describe = (channel, key, positional) => {
|
|
1106
|
+
const present = this.#queryEngine.presentTableFor(key, this.#config.table);
|
|
1107
|
+
// nulls="drop": its rows are filtered out, and dense() must not pad a
|
|
1108
|
+
// zero row for it either, or a sorted axis keeps a "(null)" slot.
|
|
1109
|
+
const nullId = this.#reactiveConfig.nulls === 'drop' ? this.#queryEngine.identityFor(key).nullId : null;
|
|
986
1110
|
identity[channel] = {
|
|
987
1111
|
dim: this.#queryEngine.dimTableFor(key),
|
|
988
1112
|
size: this.#queryEngine.identityFor(key).values.length,
|
|
989
|
-
positional
|
|
1113
|
+
positional,
|
|
1114
|
+
...(present ? { present } : {}),
|
|
1115
|
+
...(nullId !== null ? { dropNullId: nullId } : {})
|
|
990
1116
|
};
|
|
991
1117
|
};
|
|
992
1118
|
for (const { axis, key } of axes)
|
|
@@ -1057,26 +1183,33 @@ export class MarkLifecycleV2 {
|
|
|
1057
1183
|
// be reserved before a transform can write it. Reserved for every
|
|
1058
1184
|
// categorical field of this mark rather than guessing which axis topN
|
|
1059
1185
|
// will pick — at most two, and the transform looks its own field up.
|
|
1186
|
+
const otherScales = new Set();
|
|
1060
1187
|
if (this.#transformCompiler.hasSyntheticOtherBucket()) {
|
|
1061
1188
|
const otherIds = {};
|
|
1062
1189
|
for (const { axis, key } of axes) {
|
|
1063
|
-
const id = await this.#queryEngine.reserveSyntheticIdentity(key, 'Other');
|
|
1064
|
-
otherIds[axis] = id;
|
|
1065
|
-
// Re-adopt: the reservation appended to the value array, and a scale
|
|
1066
|
-
// that adopted before it must see the new entry.
|
|
1067
1190
|
const scale = (axis === 'x' ? this.#chartContext.xScaleState : this.#chartContext.yScaleState);
|
|
1068
|
-
|
|
1069
|
-
//
|
|
1070
|
-
//
|
|
1071
|
-
//
|
|
1191
|
+
// Always there by now: `axes` holds only axes whose resolved type is
|
|
1192
|
+
// band or point, and ChartScales resolves the scale instance eagerly
|
|
1193
|
+
// from registerScaleType, in this mark's constructor, so the scale
|
|
1194
|
+
// exists before setup runs. The guard is for the type, not a case.
|
|
1195
|
+
if (!scale)
|
|
1196
|
+
continue;
|
|
1197
|
+
// One Other per band scale, shared by every mark on it (see
|
|
1198
|
+
// retainOther). The scale also takes it as its Other id, so an
|
|
1199
|
+
// entrance can come out of Other and an exit go into it rather than
|
|
1200
|
+
// past the end of the axis.
|
|
1072
1201
|
//
|
|
1073
|
-
//
|
|
1074
|
-
//
|
|
1075
|
-
//
|
|
1076
|
-
//
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
scale
|
|
1202
|
+
// Held on EVERY categorical axis rather than re-deriving which axis
|
|
1203
|
+
// topN picked — that derivation living in two places is what silently
|
|
1204
|
+
// dropped the Other row once already. The axes topN did not pick are
|
|
1205
|
+
// unaffected: their id is on no row, so it is never displayed.
|
|
1206
|
+
otherScales.add(scale);
|
|
1207
|
+
this.#otherScales.add(scale);
|
|
1208
|
+
const other = await scale.retainOther(this.#markId, () => this.#queryEngine.acquireSyntheticIdentity(key, 'Other'));
|
|
1209
|
+
otherIds[axis] = other.id;
|
|
1210
|
+
// Re-adopt: acquiring may have appended to the value array, and a
|
|
1211
|
+
// scale that adopted before it must see the new entry.
|
|
1212
|
+
scale.setIdentity(this.#queryEngine.identityFor(key));
|
|
1080
1213
|
}
|
|
1081
1214
|
// topN buckets past its cutoff into these ids. The query parts rebuild:
|
|
1082
1215
|
// anything assembled before the reservation had no id and left Other
|
|
@@ -1097,6 +1230,79 @@ export class MarkLifecycleV2 {
|
|
|
1097
1230
|
this.#setIdentity(withOther);
|
|
1098
1231
|
}
|
|
1099
1232
|
}
|
|
1233
|
+
// Scales this mark no longer draws an Other on (its axis or transform
|
|
1234
|
+
// changed) get their claim back.
|
|
1235
|
+
for (const scale of this.#otherScales) {
|
|
1236
|
+
if (!otherScales.has(scale))
|
|
1237
|
+
this.#releaseOther(scale);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
#warnedFollow = false;
|
|
1241
|
+
/**
|
|
1242
|
+
* Whether this mark follows its band axes instead of adding its categories to
|
|
1243
|
+
* them (`contributeDomain={false}`). It cannot also choose its own categories
|
|
1244
|
+
* or their order: a topN or a ranking would be what a follower exists not to
|
|
1245
|
+
* do, and an explicit domain is a contribution. Such a mark is refused, once,
|
|
1246
|
+
* and contributes as usual.
|
|
1247
|
+
*/
|
|
1248
|
+
get #followsDomain() {
|
|
1249
|
+
if (this.#reactiveConfig.contributeDomain !== false)
|
|
1250
|
+
return false;
|
|
1251
|
+
const reason = this.#transformCompiler.hasCategoryReducingTransforms()
|
|
1252
|
+
? 'a category-reducing transform (topN)'
|
|
1253
|
+
: this.#rankingSort
|
|
1254
|
+
? 'a sort'
|
|
1255
|
+
: this.#reactiveConfig.domain
|
|
1256
|
+
? 'an explicit domain'
|
|
1257
|
+
: null;
|
|
1258
|
+
if (!reason)
|
|
1259
|
+
return true;
|
|
1260
|
+
if (!this.#warnedFollow) {
|
|
1261
|
+
this.#warnedFollow = true;
|
|
1262
|
+
console.error(`[${this.#markId}] Unsupported: contributeDomain={false} with ${reason}. A mark that ` +
|
|
1263
|
+
`follows its band axis cannot choose its own categories or their order. It will ` +
|
|
1264
|
+
`contribute to the domain as usual.`);
|
|
1265
|
+
}
|
|
1266
|
+
return false;
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* The band scale of an axis, for registering this mark's categories on it:
|
|
1270
|
+
* null for a mark that follows, so it registers nothing and withdraws what it
|
|
1271
|
+
* had (the prop can change). The scale still draws the mark's rows, only those
|
|
1272
|
+
* of categories it shows.
|
|
1273
|
+
*/
|
|
1274
|
+
#bandScaleForDomain(axis) {
|
|
1275
|
+
const scale = (axis === 'x' ? this.#chartContext.xScaleState : this.#chartContext.yScaleState);
|
|
1276
|
+
// Only a band or point axis has categories to follow; a value axis is
|
|
1277
|
+
// the mark's own.
|
|
1278
|
+
const type = axis === 'x' ? this.actualXScaleType : this.actualYScaleType;
|
|
1279
|
+
if ((type !== 'band' && type !== 'point') || !this.#followsDomain)
|
|
1280
|
+
return scale;
|
|
1281
|
+
scale?.removeMark(this.#markId);
|
|
1282
|
+
return null;
|
|
1283
|
+
}
|
|
1284
|
+
/** What this mark's Other rows hold, and what is done with it. */
|
|
1285
|
+
otherRows;
|
|
1286
|
+
/** What the mark is, for whether its Other bar splits; null before setup completes. */
|
|
1287
|
+
get #splitPlan() {
|
|
1288
|
+
if (!this.setupComplete)
|
|
1289
|
+
return null;
|
|
1290
|
+
const markType = this.#transformConfig.markType;
|
|
1291
|
+
const length = markType === 'barx' ? this.#transformConfig.x : this.#transformConfig.y;
|
|
1292
|
+
return {
|
|
1293
|
+
markType,
|
|
1294
|
+
length,
|
|
1295
|
+
by: this.#transformCompiler.otherBy(),
|
|
1296
|
+
expr: length ? this.#config.select[length] : undefined,
|
|
1297
|
+
hasOther: this.#transformCompiler.hasSyntheticOtherBucket(),
|
|
1298
|
+
hasJsTransforms: this.#transformCompiler.jsTransforms.length > 0
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
/** The band scales whose Other bucket this mark holds a claim on. */
|
|
1302
|
+
#otherScales = new Set();
|
|
1303
|
+
#releaseOther(scale) {
|
|
1304
|
+
this.#otherScales.delete(scale);
|
|
1305
|
+
scale.releaseOther(this.#markId, (other) => this.#queryEngine.releaseSyntheticIdentity(other));
|
|
1100
1306
|
}
|
|
1101
1307
|
/**
|
|
1102
1308
|
* The one writer of identity: the reactive copy the query parts read, and
|
|
@@ -1151,10 +1357,12 @@ export class MarkLifecycleV2 {
|
|
|
1151
1357
|
// Build raw query parts directly from config.select (no identity columns,
|
|
1152
1358
|
// no bin resolution — scale states aren't ready yet during Phase 1).
|
|
1153
1359
|
const parts = buildQueryParts(this.#config.select, this.#config._orderConfig ?? null, this.#transformCompiler.hasSQLTransforms());
|
|
1360
|
+
// Domains from the rows the mark draws: without what nulls="drop" leaves
|
|
1361
|
+
// out. This select is raw, so the filter is on the raw values.
|
|
1154
1362
|
const q = assembleQuery({
|
|
1155
1363
|
table: this.#config.table,
|
|
1156
1364
|
parts,
|
|
1157
|
-
filter,
|
|
1365
|
+
filter: [...filter, ...this.#rawNullFilters(this.#config.select)],
|
|
1158
1366
|
transformCompiler: this.#transformCompiler
|
|
1159
1367
|
});
|
|
1160
1368
|
if (!q)
|
|
@@ -1173,7 +1381,7 @@ export class MarkLifecycleV2 {
|
|
|
1173
1381
|
buildViewportCountQuery(xLo, xHi, yLo, yHi) {
|
|
1174
1382
|
if (!this.setupComplete || !this.#resolvedQueryParts)
|
|
1175
1383
|
return null;
|
|
1176
|
-
const filter = this.#filterGroup.selection.predicate(this.#
|
|
1384
|
+
const filter = this.#filterGroup.selection.predicate(this.#route) ?? [];
|
|
1177
1385
|
const q = assembleQuery({
|
|
1178
1386
|
table: this.#sourceTable,
|
|
1179
1387
|
parts: this.#resolvedQueryParts,
|
|
@@ -1210,6 +1418,43 @@ export class MarkLifecycleV2 {
|
|
|
1210
1418
|
get channelFieldMap() {
|
|
1211
1419
|
return buildChannelFieldMap(this.#config.select);
|
|
1212
1420
|
}
|
|
1421
|
+
// --- Selection routing ---
|
|
1422
|
+
/**
|
|
1423
|
+
* The identity keys of the categorical fields this mark's renderer reads
|
|
1424
|
+
* selection activity for, sorted: its ordinal colour and its band axes, as
|
|
1425
|
+
* activity-reads.ts says the renderer reads them. A categorical selection on
|
|
1426
|
+
* one of these dims the mark; on any other field it filters it.
|
|
1427
|
+
*/
|
|
1428
|
+
#reads = $derived.by(() => {
|
|
1429
|
+
const renderer = rendererForMark(this.#config.markType);
|
|
1430
|
+
if (!renderer)
|
|
1431
|
+
return NO_READS;
|
|
1432
|
+
const color = this.#colorScaleState;
|
|
1433
|
+
const bandKey = (axis) => {
|
|
1434
|
+
const type = axis === 'x' ? this.actualXScaleType : this.actualYScaleType;
|
|
1435
|
+
if (type !== 'band' && type !== 'point')
|
|
1436
|
+
return null;
|
|
1437
|
+
const band = this.#chartContext[`${axis}ScaleState`];
|
|
1438
|
+
return band?.identityKey ?? null;
|
|
1439
|
+
};
|
|
1440
|
+
return readsForResponse(this.#reactiveConfig.selectionResponse, readsFor(activityReads(renderer, this.#config.ruleExtent ?? false), {
|
|
1441
|
+
color: color?.isOrdinal ? color.identityKey : null,
|
|
1442
|
+
x: bandKey('x'),
|
|
1443
|
+
y: bandKey('y')
|
|
1444
|
+
}));
|
|
1445
|
+
});
|
|
1446
|
+
/** `#reads` as one string, so the registration effect re-runs only on a real change. */
|
|
1447
|
+
#readsKey = $derived(this.#reads.join('\u001f'));
|
|
1448
|
+
/** Built at registration; the engine and every predicate call hold this object. */
|
|
1449
|
+
#route = { reads: NO_READS, chart: null };
|
|
1450
|
+
/**
|
|
1451
|
+
* The page-wide index of this mark's filter group. Its renderer stores it in
|
|
1452
|
+
* MarkConstants to read its group's run of each field's activity block.
|
|
1453
|
+
*/
|
|
1454
|
+
get groupIndex() {
|
|
1455
|
+
// NO_GROUP for selectionResponse 'filter', which never dims.
|
|
1456
|
+
return groupIndexForResponse(this.#reactiveConfig.selectionResponse, this.#filterGroup.index);
|
|
1457
|
+
}
|
|
1213
1458
|
get dataVersion() {
|
|
1214
1459
|
return this.#handle?.dataVersion ?? 0;
|
|
1215
1460
|
}
|
|
@@ -1409,10 +1654,14 @@ export class MarkLifecycleV2 {
|
|
|
1409
1654
|
if (orderCol)
|
|
1410
1655
|
orderClause = ` ORDER BY ${orderCol}`;
|
|
1411
1656
|
}
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1657
|
+
// Without Other's member lists: every hover query is `SELECT *` from
|
|
1658
|
+
// this table, so lists of up to the field's cardinality would ship on
|
|
1659
|
+
// each one. Only those, not every `__dplot_` column as Path B drops:
|
|
1660
|
+
// the rest are scalars (the rank, id columns), cheap to keep, and the
|
|
1661
|
+
// lists are the only ones whose size grows with the field.
|
|
1662
|
+
const withoutMembers = withoutOtherMembers(tooltipQuery);
|
|
1663
|
+
// One statement: a hover query never finds the table missing.
|
|
1664
|
+
await this.#queryEngine.exec(`CREATE OR REPLACE TEMP TABLE "${tableName}" AS SELECT *${replaceClause} FROM (${withoutMembers})${orderClause}`);
|
|
1416
1665
|
}
|
|
1417
1666
|
else {
|
|
1418
1667
|
// Path B: Serialize transformed data via SQL VALUES.
|
|
@@ -1467,6 +1716,9 @@ export class MarkLifecycleV2 {
|
|
|
1467
1716
|
}
|
|
1468
1717
|
}
|
|
1469
1718
|
this.#tooltipTableReady = true;
|
|
1719
|
+
// The row under a still pointer was read from the old table: read it
|
|
1720
|
+
// again, so the tooltip and a click show the new data without a move.
|
|
1721
|
+
this.#chartContext.tooltipManager?.refresh();
|
|
1470
1722
|
this.#onTooltipTableWritten();
|
|
1471
1723
|
}
|
|
1472
1724
|
catch (error) {
|
|
@@ -1480,7 +1732,7 @@ export class MarkLifecycleV2 {
|
|
|
1480
1732
|
#buildTooltipQuery() {
|
|
1481
1733
|
if (!this.#resolvedQueryParts)
|
|
1482
1734
|
return null;
|
|
1483
|
-
const filter = this.#filterGroup.selection.predicate(this.#
|
|
1735
|
+
const filter = this.#filterGroup.selection.predicate(this.#route) ?? [];
|
|
1484
1736
|
const q = assembleQuery({
|
|
1485
1737
|
table: this.#sourceTable,
|
|
1486
1738
|
parts: this.#resolvedQueryParts,
|
|
@@ -1537,7 +1789,10 @@ export class MarkLifecycleV2 {
|
|
|
1537
1789
|
}
|
|
1538
1790
|
Object.assign(sidecarSelect, tipExtras);
|
|
1539
1791
|
const sidecarParts = buildQueryParts(sidecarSelect, undefined, this.#transformCompiler.hasSQLTransforms());
|
|
1540
|
-
|
|
1792
|
+
// The same rows as the mark: it leaves out what the mark leaves out. Its
|
|
1793
|
+
// select holds every non-aggregate channel, so each dropped one is there.
|
|
1794
|
+
sidecarParts.nullDrops = this.#resolvedQueryParts.nullDrops;
|
|
1795
|
+
const filter = this.#filterGroup.selection.predicate(this.#route) ?? [];
|
|
1541
1796
|
const sidecarQuery = assembleQuery({
|
|
1542
1797
|
table: this.#sourceTable,
|
|
1543
1798
|
parts: sidecarParts,
|
|
@@ -1735,6 +1990,9 @@ export class MarkLifecycleV2 {
|
|
|
1735
1990
|
this.#destroyed = true;
|
|
1736
1991
|
this.#identityResetOff?.();
|
|
1737
1992
|
this.#identityResetOff = null;
|
|
1993
|
+
this.otherRows.unregister();
|
|
1994
|
+
for (const scale of [...this.#otherScales])
|
|
1995
|
+
this.#releaseOther(scale);
|
|
1738
1996
|
this.#handle?.destroy();
|
|
1739
1997
|
clearTimeout(this.#fitDomainTimeout);
|
|
1740
1998
|
clearTimeout(this.#tooltipWriteTimeout);
|
|
@@ -14,7 +14,9 @@ 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): Promise<number>;
|
|
18
20
|
/**
|
|
19
21
|
* Fetch global min/max extents for aggregated fields using a "sidecar" query.
|
|
20
22
|
* Runs the same aggregation as the main query but without filters, wrapped
|
|
@@ -10,7 +10,13 @@ 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
|
+
const categoriesOf = (key) => {
|
|
17
|
+
const categories = coordinator.categoryValues(key, table);
|
|
18
|
+
return dropNull ? categories.filter((c) => c !== null) : categories;
|
|
19
|
+
};
|
|
14
20
|
const fetches = [];
|
|
15
21
|
const setupFetch = (axis, field, scaleState) => {
|
|
16
22
|
if (!field || !scaleState)
|
|
@@ -42,10 +48,11 @@ export async function fetchCategoricalDomains(coordinator, table, select, actual
|
|
|
42
48
|
//
|
|
43
49
|
// - Order. DISTINCT has no ORDER BY, so today's domain is whatever DuckDB
|
|
44
50
|
// returned, then "first-appearance order" in updateDomainFromExtents.
|
|
45
|
-
// Arbitrary. Dim
|
|
46
|
-
// deterministic
|
|
47
|
-
//
|
|
48
|
-
//
|
|
51
|
+
// Arbitrary. Dim values are listed in id order, so the domain is
|
|
52
|
+
// deterministic. It is the real categories only (`categoryValues`
|
|
53
|
+
// leaves out Other buckets), so position-in-domain equals the dim id
|
|
54
|
+
// only until a bucket's row: the scale encodes each value to its id
|
|
55
|
+
// rather than relying on position.
|
|
49
56
|
// - Multi-table. The dim spans every table registering the field, so two
|
|
50
57
|
// marks on one shared scale agree on the category set by construction.
|
|
51
58
|
//
|
|
@@ -63,10 +70,11 @@ export async function fetchCategoricalDomains(coordinator, table, select, actual
|
|
|
63
70
|
fetches.push((async () => {
|
|
64
71
|
// Idempotent and coalesced — the mark's own setup calls this too.
|
|
65
72
|
await coordinator.ensureColumns(table, { categorical: [identityField] });
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
//
|
|
69
|
-
//
|
|
73
|
+
// This table's categories: the dim spans every table on the field.
|
|
74
|
+
const categories = categoriesOf(identityField.key);
|
|
75
|
+
// A snapshot of the real categories. The scale adopts the live
|
|
76
|
+
// identity itself in MarkLifecycleV2's #ensureIdentityColumns, which
|
|
77
|
+
// also covers the explicit-domain axes that never reach this branch.
|
|
70
78
|
if (categories.length > 0)
|
|
71
79
|
scaleState.addMark(categories, markId);
|
|
72
80
|
})());
|
|
@@ -101,9 +109,8 @@ export async function fetchCategoricalDomains(coordinator, table, select, actual
|
|
|
101
109
|
// Registered from the data.
|
|
102
110
|
}
|
|
103
111
|
else if (explicitDomain?.y && yScaleState) {
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
deferredExplicitY = [...explicitDomain.y].reverse();
|
|
112
|
+
// Slot 0 is at the top of a y band axis, so the list is used as given.
|
|
113
|
+
deferredExplicitY = explicitDomain.y;
|
|
107
114
|
}
|
|
108
115
|
else {
|
|
109
116
|
setupFetch('y', select.y, yScaleState);
|
|
@@ -121,7 +128,7 @@ export async function fetchCategoricalDomains(coordinator, table, select, actual
|
|
|
121
128
|
fetches.push((async () => {
|
|
122
129
|
// Idempotent and coalesced — the mark's own setup calls this too.
|
|
123
130
|
await coordinator.ensureColumns(table, { categorical: [facetKey] });
|
|
124
|
-
const categories =
|
|
131
|
+
const categories = categoriesOf(facetKey.key);
|
|
125
132
|
if (categories.length > 0)
|
|
126
133
|
facetBandScale.addMark(categories, markId);
|
|
127
134
|
})());
|
|
@@ -185,7 +192,7 @@ export async function fetchCategoricalDomains(coordinator, table, select, actual
|
|
|
185
192
|
fetches.push((async () => {
|
|
186
193
|
// Idempotent and coalesced — the mark's own setup calls this too.
|
|
187
194
|
await coordinator.ensureColumns(table, { categorical: [colorIdentity] });
|
|
188
|
-
const categories = coordinator.
|
|
195
|
+
const categories = coordinator.categoryValues(colorIdentity.key);
|
|
189
196
|
if (categories.length > 0) {
|
|
190
197
|
colorScaleState.setDomain(categories);
|
|
191
198
|
}
|
|
@@ -234,7 +241,7 @@ export async function fetchCategoricalDomains(coordinator, table, select, actual
|
|
|
234
241
|
if (seriesIdentity) {
|
|
235
242
|
fetches.push((async () => {
|
|
236
243
|
await coordinator.ensureColumns(table, { categorical: [seriesIdentity] });
|
|
237
|
-
const categories = coordinator.
|
|
244
|
+
const categories = coordinator.categoryValues(seriesIdentity.key);
|
|
238
245
|
if (categories.length > 0) {
|
|
239
246
|
seriesScaleState.setDomain(categories);
|
|
240
247
|
}
|
|
@@ -193,12 +193,8 @@ export function registerDataDerivedDomains(data, channelsNeedingDataDomain, addi
|
|
|
193
193
|
// order the ranked query had. See RANK_COLUMN.
|
|
194
194
|
const ranked = data.getColumn(RANK_COLUMN) != null;
|
|
195
195
|
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();
|
|
196
|
+
// Used as ranked on both axes: slot 0 is at the left of x and the
|
|
197
|
+
// top of y, where a ranking is read from.
|
|
202
198
|
if (ids.length === 0)
|
|
203
199
|
continue;
|
|
204
200
|
// `sorted` says this list is a ranking the axis should prefer over
|