@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.
Files changed (148) hide show
  1. package/README.md +2 -0
  2. package/dist/components/axes/AxisBottom.svelte +23 -6
  3. package/dist/components/axes/AxisBottom.svelte.d.ts +10 -0
  4. package/dist/components/axes/AxisLeft.svelte +77 -8
  5. package/dist/components/axes/AxisLeft.svelte.d.ts +17 -0
  6. package/dist/components/axes/BandAxisTicks.svelte +64 -25
  7. package/dist/components/axes/BandAxisTicks.svelte.d.ts +5 -5
  8. package/dist/components/axes/axis-title.d.ts +30 -0
  9. package/dist/components/axes/axis-title.js +77 -0
  10. package/dist/components/charts/ChartCell.svelte +1 -1
  11. package/dist/components/charts/ChartContainer.svelte +12 -3
  12. package/dist/components/charts/ChartContainer.svelte.d.ts +7 -0
  13. package/dist/components/guides/legend/ContinuousColorLegend.svelte +9 -2
  14. package/dist/components/guides/legend/OrdinalColorLegend.svelte +96 -35
  15. package/dist/components/marks/AreaMarkX.svelte +21 -6
  16. package/dist/components/marks/AreaMarkY.svelte +21 -6
  17. package/dist/components/marks/BarMarkX.svelte +16 -1
  18. package/dist/components/marks/BarMarkY.svelte +16 -1
  19. package/dist/components/marks/DotsMark.svelte +5 -0
  20. package/dist/components/marks/LineMark.svelte +8 -1
  21. package/dist/components/marks/ParallelCoordinatesMark.svelte +10 -0
  22. package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +5 -0
  23. package/dist/components/marks/RectMark.svelte +5 -0
  24. package/dist/components/marks/RectXMark.svelte +5 -0
  25. package/dist/components/marks/RectYMark.svelte +5 -0
  26. package/dist/components/marks/RuleMarkX.svelte +12 -2
  27. package/dist/components/marks/RuleMarkY.svelte +12 -2
  28. package/dist/components/marks/TextMark.svelte +76 -13
  29. package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +21 -2
  30. package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +300 -42
  31. package/dist/components/marks/lifecycle/domain-queries.d.ts +3 -1
  32. package/dist/components/marks/lifecycle/domain-queries.js +22 -15
  33. package/dist/components/marks/lifecycle/domain-registration.js +2 -6
  34. package/dist/components/marks/lifecycle/other-rows.svelte.d.ts +104 -0
  35. package/dist/components/marks/lifecycle/other-rows.svelte.js +193 -0
  36. package/dist/components/marks/lifecycle/other-split.d.ts +80 -0
  37. package/dist/components/marks/lifecycle/other-split.js +112 -0
  38. package/dist/components/marks/lifecycle/query-builder.d.ts +26 -1
  39. package/dist/components/marks/lifecycle/query-builder.js +25 -5
  40. package/dist/components/marks/lifecycle/transform-scale-key.d.ts +16 -0
  41. package/dist/components/marks/lifecycle/transform-scale-key.js +27 -0
  42. package/dist/components/marks/utils/area-outline.d.ts +24 -0
  43. package/dist/components/marks/utils/area-outline.js +10 -0
  44. package/dist/components/marks/utils/reactive-config.d.ts +1 -1
  45. package/dist/components/marks/utils/reactive-config.js +9 -0
  46. package/dist/components/marks/utils/tooltip-helpers.d.ts +4 -1
  47. package/dist/components/marks/utils/tooltip-helpers.js +2 -1
  48. package/dist/components/rendering/Areas.svelte +30 -6
  49. package/dist/components/rendering/Areas.svelte.d.ts +2 -0
  50. package/dist/components/rendering/BaseGPURenderer.svelte.d.ts +3 -1
  51. package/dist/components/rendering/BaseGPURenderer.svelte.js +8 -3
  52. package/dist/components/rendering/Dots.svelte +24 -3
  53. package/dist/components/rendering/Dots.svelte.d.ts +2 -0
  54. package/dist/components/rendering/GridLine.svelte +2 -1
  55. package/dist/components/rendering/Lines.svelte +49 -7
  56. package/dist/components/rendering/Lines.svelte.d.ts +9 -0
  57. package/dist/components/rendering/ParallelLines.svelte +26 -7
  58. package/dist/components/rendering/ParallelLines.svelte.d.ts +2 -0
  59. package/dist/components/rendering/Rects.svelte +129 -20
  60. package/dist/components/rendering/Rects.svelte.d.ts +9 -0
  61. package/dist/components/rendering/RuleX.svelte +20 -5
  62. package/dist/components/rendering/RuleX.svelte.d.ts +2 -0
  63. package/dist/components/rendering/RuleY.svelte +20 -5
  64. package/dist/components/rendering/RuleY.svelte.d.ts +2 -0
  65. package/dist/components/rendering/activity-reads.d.ts +111 -0
  66. package/dist/components/rendering/activity-reads.js +154 -0
  67. package/dist/components/rendering/uniform-sizes.d.ts +26 -0
  68. package/dist/components/rendering/uniform-sizes.js +28 -0
  69. package/dist/config/ChartConfig.svelte.d.ts +5 -1
  70. package/dist/gpu/ColorScaleShaderLib.d.ts +13 -1
  71. package/dist/gpu/ColorScaleShaderLib.js +56 -5
  72. package/dist/gpu/GPUCoordinator.svelte.d.ts +8 -0
  73. package/dist/gpu/GPUCoordinator.svelte.js +68 -32
  74. package/dist/gpu/MorphedChannels.d.ts +6 -32
  75. package/dist/gpu/MorphedChannels.js +45 -3
  76. package/dist/gpu/ResolveShader.d.ts +2 -2
  77. package/dist/gpu/ResolveShader.js +2 -2
  78. package/dist/gpu/ScaleTemplateSystem.d.ts +19 -0
  79. package/dist/gpu/ScaleTemplateSystem.js +111 -11
  80. package/dist/gpu/SizeScaleShaderLib.d.ts +4 -4
  81. package/dist/gpu/SizeScaleShaderLib.js +4 -4
  82. package/dist/gpu/identity-slot-packing.d.ts +157 -40
  83. package/dist/gpu/identity-slot-packing.js +262 -62
  84. package/dist/gpu/tracked-scales.d.ts +14 -0
  85. package/dist/gpu/tracked-scales.js +18 -0
  86. package/dist/index.d.ts +2 -0
  87. package/dist/index.js +4 -0
  88. package/dist/interactors/BandLabelToggle.svelte +5 -9
  89. package/dist/interactors/BandLabelToggle.svelte.d.ts +3 -3
  90. package/dist/interactors/DerivedInterval1D.svelte +26 -54
  91. package/dist/interactors/Interval1D.svelte +8 -2
  92. package/dist/interactors/Interval2D.svelte +8 -2
  93. package/dist/interactors/ParallelAxisSource.svelte +14 -5
  94. package/dist/interactors/ParallelAxisSource.svelte.d.ts +2 -1
  95. package/dist/interactors/ParallelBrushes.svelte +8 -9
  96. package/dist/interactors/ParallelBrushes.svelte.d.ts +3 -3
  97. package/dist/interactors/Toggle.svelte +48 -38
  98. package/dist/interactors/Toggle.svelte.d.ts +4 -3
  99. package/dist/interactors/Tooltip.svelte +140 -42
  100. package/dist/interactors/TooltipManager.svelte.d.ts +20 -5
  101. package/dist/interactors/TooltipManager.svelte.js +40 -10
  102. package/dist/interactors/other-tooltip.d.ts +16 -0
  103. package/dist/interactors/other-tooltip.js +22 -0
  104. package/dist/interactors/relay-clauses.d.ts +23 -0
  105. package/dist/interactors/relay-clauses.js +60 -0
  106. package/dist/query-engine/CrossfilterSelection.svelte.d.ts +88 -15
  107. package/dist/query-engine/CrossfilterSelection.svelte.js +94 -20
  108. package/dist/query-engine/FieldActivity.svelte.d.ts +128 -0
  109. package/dist/query-engine/FieldActivity.svelte.js +378 -0
  110. package/dist/query-engine/FilterGroup.svelte.d.ts +76 -30
  111. package/dist/query-engine/FilterGroup.svelte.js +223 -124
  112. package/dist/query-engine/FilterGroupRegistry.svelte.d.ts +73 -3
  113. package/dist/query-engine/FilterGroupRegistry.svelte.js +161 -12
  114. package/dist/query-engine/MarkHandle.svelte.d.ts +9 -1
  115. package/dist/query-engine/MarkHandle.svelte.js +12 -2
  116. package/dist/query-engine/QueryEngine.d.ts +52 -16
  117. package/dist/query-engine/QueryEngine.js +116 -34
  118. package/dist/query-engine/categorical-identity.d.ts +114 -27
  119. package/dist/query-engine/categorical-identity.js +225 -51
  120. package/dist/query-engine/index.d.ts +2 -1
  121. package/dist/query-engine/index.js +1 -0
  122. package/dist/query-engine/preagg/Preagg.d.ts +1 -1
  123. package/dist/query-engine/preagg/Preagg.js +11 -9
  124. package/dist/query-engine/preagg/plan.d.ts +14 -5
  125. package/dist/query-engine/preagg/plan.js +44 -18
  126. package/dist/query-engine/toggle-predicate.d.ts +21 -0
  127. package/dist/query-engine/toggle-predicate.js +41 -2
  128. package/dist/scales/ActivityTable.d.ts +103 -0
  129. package/dist/scales/ActivityTable.js +220 -0
  130. package/dist/scales/BandScaleState.svelte.d.ts +41 -15
  131. package/dist/scales/BandScaleState.svelte.js +78 -36
  132. package/dist/scales/ChartScales.svelte.d.ts +2 -2
  133. package/dist/scales/ColorScaleState.svelte.d.ts +36 -8
  134. package/dist/scales/ColorScaleState.svelte.js +118 -24
  135. package/dist/scales/DomainResolver.d.ts +2 -0
  136. package/dist/scales/DomainResolver.js +15 -8
  137. package/dist/scales/ScaleRegistry.svelte.d.ts +9 -0
  138. package/dist/scales/ScaleRegistry.svelte.js +22 -1
  139. package/dist/scales/TimeScaleState.svelte.js +7 -1
  140. package/dist/spec/index.js +1 -0
  141. package/dist/spec/types.d.ts +4 -0
  142. package/dist/theme.css +5 -4
  143. package/dist/transforms/TransformCompiler.d.ts +37 -2
  144. package/dist/transforms/TransformCompiler.js +44 -1
  145. package/dist/transforms/index.d.ts +27 -0
  146. package/dist/transforms/index.js +127 -19
  147. package/dist/types.d.ts +71 -0
  148. 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 onFilterChange().
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 = { ...config, transformKey: `${table}:${key}` };
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 = { ...config, transformKey: `${table}:${key}` };
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: write tooltip table when data updates (debounced).
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
- const tooltipWanted = tooltipConfig !== false || !!fitDomain;
478
- if (!tooltipWanted)
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
- // Cleanup: when tooltip goes true→false, drop the temp table.
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.#chartContext.xScaleState, this.#chartContext.yScaleState, 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),
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.#chartContext.xScaleState, this.#chartContext.yScaleState, this.#markId, false, // isRebinning
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
- scale?.setIdentity(this.#queryEngine.identityFor(key));
1069
- // And tell it WHICH id is the bucket, so an entrance can come out of
1070
- // Other and an exit can go into it rather than past the end of the
1071
- // axis. With an Other bucket that is where the value actually goes.
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
- // Set on EVERY categorical axis, like the reservation above, rather
1074
- // than re-deriving which axis topN picked — that derivation living
1075
- // in two places is what silently dropped the Other row once already.
1076
- // The axes topN did not pick are unaffected: their reserved id is on
1077
- // no row, so it is never displayed, and setOtherId documents why an
1078
- // undisplayed id cannot act as a gateway.
1079
- scale?.setOtherId(id);
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.#markId) ?? [];
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
- await this.#queryEngine.exec([
1413
- `DROP TABLE IF EXISTS "${tableName}"`,
1414
- `CREATE TEMP TABLE "${tableName}" AS SELECT *${replaceClause} FROM (${tooltipQuery})${orderClause}`
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.#markId) ?? [];
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
- const filter = this.#filterGroup.selection.predicate(this.#markId) ?? [];
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): Promise<number>;
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 labels are ordered by id, so the domain is
46
- // deterministic — and position-in-domain now EQUALS the dim id, which
47
- // is the invariant that lets the query switch to the view's id column
48
- // without moving a single bar.
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
- const categories = coordinator.identityValues(identityField.key);
67
- // The scale adopts this same array in MarkLifecycleV2's
68
- // #ensureIdentityColumns, which also covers the explicit-domain axes
69
- // that never reach this branch.
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
- // Y band scale maps index 0 → bottom, so reverse the user's
105
- // top-to-bottom array to match GPU rendering order.
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 = coordinator.identityValues(facetKey.key);
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.identityValues(colorIdentity.key);
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.identityValues(seriesIdentity.key);
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
- // A y band axis puts slot 0 at the bottom and a ranking is read
197
- // from the top. Reversed here, once, for every ranked axis —
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