@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
@@ -12,7 +12,7 @@
12
12
  * License; see NOTICE.
13
13
  */
14
14
  import { isAggregateExpression, isNode, collectColumns, column, and, isBetween, literal, Query, sql as sqlTag } from '@uwdata/mosaic-sql';
15
- import { togglePredicate } from '../toggle-predicate';
15
+ import { BITMAP_MIN_IDS, idBitmapPredicate, togglePredicate } from '../toggle-predicate';
16
16
  import { rewriteForPreagg, buildBinExpr, binsFineEnough, BIN_WINDOW_MARGIN } from '../sufficient-statistics';
17
17
  import { assembleQuery, castNumeric, castReal } from '../../components/marks/lifecycle/query-builder';
18
18
  /**
@@ -37,6 +37,11 @@ export function planPreagg(descriptor, dims, identity) {
37
37
  const { parts, table, baseTable, schema } = descriptor;
38
38
  if (!parts.hasAggregates && !parts.hasWindowAggregates)
39
39
  return null;
40
+ // A having() on a preagg table would count its groups, not the source rows, and
41
+ // return the wrong rows without an error. The built-in transforms add none; see
42
+ // TransformCompiler.hasHaving for why, and for what supporting it would take.
43
+ if (descriptor.transformCompiler.hasHaving())
44
+ return null;
40
45
  // Analyze selectDef for preagg compatibility
41
46
  const analysis = rewriteForPreagg(parts.selectDef, parts.passthroughChannels);
42
47
  if (!analysis)
@@ -74,7 +79,8 @@ export function planPreagg(descriptor, dims, identity) {
74
79
  activeMeta.push({ kind: 'value', field, column: channel, byId: true });
75
80
  return;
76
81
  }
77
- // One more group for -1: NULL, or a value the dim does not hold.
82
+ // One more group for -1: a value the dim does not hold yet. NULL has an id
83
+ // of its own, counted in the values.
78
84
  const idGroups = identity.values(field).length + 1;
79
85
  if (identity.dimCovers(field, baseTable)) {
80
86
  // Case 2.
@@ -114,6 +120,7 @@ export function planPreagg(descriptor, dims, identity) {
114
120
  groupby: [...preaggGroupBy, ...activeGroupBy],
115
121
  // Sorted by bin columns for fast range scans
116
122
  orderby: binOrder,
123
+ where: descriptor.transformCompiler.rowFilters(),
117
124
  joins
118
125
  },
119
126
  outputSelect,
@@ -217,16 +224,16 @@ export function estimateGroups(markKeys, activeKeys) {
217
224
  * mismatch the table's SQL is rebuilt from the current clauses and compared.
218
225
  * Once that fails, frames that still see the same clauses skip the rebuild.
219
226
  */
220
- export function bakedClausesHold(markId, selection, state) {
227
+ export function bakedClausesHold(route, selection, state) {
221
228
  const { basis } = state;
222
229
  const { activeSourceId } = basis;
223
- if (selection.bakedClausesMatch(markId, activeSourceId, basis.bakedClauses))
230
+ if (selection.bakedClausesMatch(route, activeSourceId, basis.bakedClauses))
224
231
  return true;
225
- if (state.mismatch && selection.bakedClausesMatch(markId, activeSourceId, state.mismatch)) {
232
+ if (state.mismatch && selection.bakedClausesMatch(route, activeSourceId, state.mismatch)) {
226
233
  return false;
227
234
  }
228
- const current = selection.bakedClauses(markId, activeSourceId);
229
- const sql = preaggSelectSQL(state.creation, selection.bakedPredicates(markId, activeSourceId));
235
+ const current = selection.bakedClauses(route, activeSourceId);
236
+ const sql = preaggSelectSQL(state.creation, selection.bakedPredicates(route, activeSourceId));
230
237
  if (sql === state.sql) {
231
238
  basis.bakedClauses = current;
232
239
  state.mismatch = null;
@@ -241,15 +248,16 @@ export function bakedClausesHold(markId, selection, state) {
241
248
  * Builds modified QueryParts from the stored outputSelect + bin filters,
242
249
  * then delegates to assembleQuery().
243
250
  */
244
- export function preaggQuerySQL(markId, descriptor, selection, preagg, params, identity) {
251
+ export function preaggQuerySQL(route, descriptor, selection, preagg, params, identity) {
245
252
  const { activeMeta, outputSelect, windowChannels } = preagg;
246
253
  const { parts, transformCompiler } = descriptor;
247
254
  // --- The active source's clause as a filter on the active columns. None
248
- // when it has no clause (an empty toggle: every category) or excludes
249
- // this mark.
255
+ // when it has no clause (an empty toggle: every category) or does not
256
+ // filter this mark.
250
257
  const filters = [];
251
- const clause = selection.get(preagg.basis.activeSourceId);
252
- if (clause && !clause.excludeMarkIds.has(markId)) {
258
+ const activeSourceId = preagg.basis.activeSourceId;
259
+ const clause = selection.get(activeSourceId);
260
+ if (clause && selection.applies(activeSourceId, clause, route)) {
253
261
  // A drag frame runs this per mark: nothing below allocates for a brush
254
262
  // beyond its filter, so the value arrays are created only when needed.
255
263
  let binCount = 0;
@@ -278,8 +286,9 @@ export function preaggQuerySQL(markId, descriptor, selection, preagg, params, id
278
286
  }
279
287
  if (valueExprs && valueMeta) {
280
288
  // A toggle's clause holds tuples in its field order. Id columns
281
- // filter by id; a value with no id (NULL, or not in the dim) cannot
282
- // be expressed, so this query goes to the base table instead.
289
+ // filter by id (the NULL category has one); a value with no id (not in
290
+ // the dim yet) cannot be expressed, so this query goes to the base
291
+ // table instead.
283
292
  const tuples = [];
284
293
  for (const tuple of clause.value) {
285
294
  const mapped = [];
@@ -292,7 +301,17 @@ export function preaggQuerySQL(markId, descriptor, selection, preagg, params, id
292
301
  }
293
302
  tuples.push(mapped);
294
303
  }
295
- filters.push(togglePredicate(valueExprs, tuples));
304
+ // One field by id, and many of them (a click on a large Other): a
305
+ // constant-size bitmap rather than a long IN list, which DuckDB spends
306
+ // most of its time parsing. Measured on a preagg table: 1 ms against
307
+ // 20 ms for IN of 9 000 ids.
308
+ const only = valueMeta.length === 1 ? valueMeta[0] : null;
309
+ if (only?.byId && tuples.length > BITMAP_MIN_IDS) {
310
+ filters.push(idBitmapPredicate(valueExprs[0], tuples.map((t) => t[0]), identity.identity(only.field).values.length));
311
+ }
312
+ else {
313
+ filters.push(togglePredicate(valueExprs, tuples));
314
+ }
296
315
  }
297
316
  }
298
317
  // --- Build selectDef: CAST recombined aggregates; dimensions are already
@@ -322,13 +341,19 @@ export function preaggQuerySQL(markId, descriptor, selection, preagg, params, id
322
341
  hasSQLTransforms: parts.hasSQLTransforms,
323
342
  orderChannels: parts.orderChannels,
324
343
  aliasMap: parts.aliasMap,
325
- passthroughChannels: parts.passthroughChannels
344
+ passthroughChannels: parts.passthroughChannels,
345
+ // nulls="drop", read through this select: each dropped channel is a group
346
+ // key, so the table keeps NULL as its own group, and dropping that group
347
+ // here removes exactly what the base scan would have left out.
348
+ nullDrops: parts.nullDrops
326
349
  };
327
350
  return assembleQuery({
328
351
  table: preagg.table,
329
352
  parts: preaggParts,
330
353
  filter: filters,
331
354
  transformCompiler,
355
+ // The table was built with them.
356
+ skipRowFilters: true,
332
357
  limit: params.limit,
333
358
  window: windowChannels ? { channels: windowChannels, cteName: '__preagg_cte' } : null
334
359
  }).toString();
@@ -336,8 +361,9 @@ export function preaggQuerySQL(markId, descriptor, selection, preagg, params, id
336
361
  /** A preagg table's SELECT. Build and validity check share it, so equal SQL means an equal table. */
337
362
  export function preaggSelectSQL(creation, filters) {
338
363
  const query = Query.from(creation.table).select(creation.select).groupby(creation.groupby);
339
- if (filters.length > 0)
340
- query.where(and(filters));
364
+ const where = [...creation.where, ...filters];
365
+ if (where.length > 0)
366
+ query.where(and(where));
341
367
  if (creation.joins.length === 0)
342
368
  return query.orderby(creation.orderby).toString();
343
369
  // Map fields grouped by value to their ids on the grouped result: a join
@@ -8,3 +8,24 @@ import { type ExprNode } from '@uwdata/mosaic-sql';
8
8
  * so a predicate is never rewritten after the fact.
9
9
  */
10
10
  export declare function togglePredicate(exprs: ExprNode[], tuples: unknown[][]): ExprNode;
11
+ /**
12
+ * Above this many ids, a selection over an id column is a bitmap, not an `IN`
13
+ * list (see idBitmapPredicate). The crossover measured in DuckDB-WASM 1.30 lies
14
+ * between 500 values (`IN`, 29 ms on 2 M rows) and the bitmap (26 ms).
15
+ */
16
+ export declare const BITMAP_MIN_IDS = 256;
17
+ /**
18
+ * `expr`'s id is one of `ids`, as one constant-size literal:
19
+ * `get_bit('<bits>'::BIT, <index>) = 1`.
20
+ *
21
+ * Never `NOT IN`, whatever the selection's size: DuckDB evaluates it as a chain
22
+ * of `<>`, 2.8 s on 2 M rows for 1 000 ids. And never a long `IN`, which it
23
+ * spends most of its time parsing.
24
+ *
25
+ * Bit `id + 1` is set for each selected id; bit 0 is id -1 ("not in the dim
26
+ * yet") and is never set; the last bit, `size + 1`, is a sentinel kept 0. The
27
+ * index is clamped onto [0, sentinel], so an id appended to the dim after the
28
+ * selection was made reads the sentinel (it was not selected: a snapshot), and
29
+ * get_bit never sees an index out of range, which is an error rather than a 0.
30
+ */
31
+ export declare function idBitmapPredicate(expr: ExprNode, ids: readonly number[], size: number): ExprNode;
@@ -1,4 +1,4 @@
1
- import { and as sqlAnd, isIn, isNotDistinct, literal, or } from '@uwdata/mosaic-sql';
1
+ import { and as sqlAnd, isIn, isNotDistinct, literal, or, sql as sqlTag } from '@uwdata/mosaic-sql';
2
2
  /**
3
3
  * A toggle's clause over its selected tuples: `IN` for one field, an `OR` of
4
4
  * `IS NOT DISTINCT FROM` tuples for several, so a NULL member matches NULL.
@@ -9,8 +9,47 @@ import { and as sqlAnd, isIn, isNotDistinct, literal, or } from '@uwdata/mosaic-
9
9
  */
10
10
  export function togglePredicate(exprs, tuples) {
11
11
  if (exprs.length === 1) {
12
- return isIn(exprs[0], tuples.map((t) => literal(t[0])));
12
+ // NULL is a category, but `IN (…, NULL)` never matches it: NULL = NULL is
13
+ // not true. It goes beside the list as IS NULL instead.
14
+ const values = tuples.map((t) => t[0]);
15
+ const real = values.filter((v) => v !== null);
16
+ const hasNull = real.length < values.length;
17
+ const inList = real.length > 0
18
+ ? isIn(exprs[0], real.map((v) => literal(v)))
19
+ : null;
20
+ const isNull = hasNull ? isNotDistinct(exprs[0], literal(null)) : null;
21
+ if (inList && isNull)
22
+ return or([inList, isNull]);
23
+ return inList ?? isNull ?? isIn(exprs[0], []);
13
24
  }
14
25
  const clauses = tuples.map((t) => sqlAnd(t.map((v, i) => isNotDistinct(exprs[i], literal(v)))));
15
26
  return clauses.length === 1 ? clauses[0] : or(clauses);
16
27
  }
28
+ /**
29
+ * Above this many ids, a selection over an id column is a bitmap, not an `IN`
30
+ * list (see idBitmapPredicate). The crossover measured in DuckDB-WASM 1.30 lies
31
+ * between 500 values (`IN`, 29 ms on 2 M rows) and the bitmap (26 ms).
32
+ */
33
+ export const BITMAP_MIN_IDS = 256;
34
+ /**
35
+ * `expr`'s id is one of `ids`, as one constant-size literal:
36
+ * `get_bit('<bits>'::BIT, <index>) = 1`.
37
+ *
38
+ * Never `NOT IN`, whatever the selection's size: DuckDB evaluates it as a chain
39
+ * of `<>`, 2.8 s on 2 M rows for 1 000 ids. And never a long `IN`, which it
40
+ * spends most of its time parsing.
41
+ *
42
+ * Bit `id + 1` is set for each selected id; bit 0 is id -1 ("not in the dim
43
+ * yet") and is never set; the last bit, `size + 1`, is a sentinel kept 0. The
44
+ * index is clamped onto [0, sentinel], so an id appended to the dim after the
45
+ * selection was made reads the sentinel (it was not selected: a snapshot), and
46
+ * get_bit never sees an index out of range, which is an error rather than a 0.
47
+ */
48
+ export function idBitmapPredicate(expr, ids, size) {
49
+ const bits = new Array(size + 2).fill('0');
50
+ for (const id of ids)
51
+ if (id >= 0 && id < size)
52
+ bits[id + 1] = '1';
53
+ const sentinel = size + 1;
54
+ return sqlTag `get_bit(${literal(bits.join(''))}::BIT, CAST(greatest(least(${expr} + 1, ${sentinel}), 0) AS INTEGER)) = 1`;
55
+ }
@@ -0,0 +1,103 @@
1
+ import { TransitionClock, type ClockLoop } from '../animation';
2
+ /**
3
+ * Stiffness for a selection fade: ~0.25 s to settle, against ~0.32 s for a data
4
+ * transition (TRANSITION_STIFFNESS, 240). A click should feel immediate; the
5
+ * charts it filters take their own time to re-query and morph.
6
+ */
7
+ export declare const SELECTION_STIFFNESS = 400;
8
+ /**
9
+ * How visible an inactive category is, by default: marks are drawn at this
10
+ * opacity (the shaders' SELECTION_DIM_STRENGTH is 1 - this), and
11
+ * band labels fade to it.
12
+ */
13
+ export declare const DEFAULT_INACTIVE_OPACITY = 0.25;
14
+ /**
15
+ * How visible a category is for its activity: `inactiveOpacity` when
16
+ * unselected, 1 when selected, and in between mid-fade.
17
+ *
18
+ * The CPU twin of the shaders' `selectionDim`, which computes the same thing as
19
+ * `1 - dimStrength * (1 - activity)` with `dimStrength = 1 - inactiveOpacity`.
20
+ * Band labels and legend entries use this, so they match the marks.
21
+ */
22
+ export declare function selectionOpacity(activity: number, inactiveOpacity?: number): number;
23
+ /**
24
+ * Which categories of a scale are selected, and how far each one has faded.
25
+ *
26
+ * One float per identity id: 1 active, 0 inactive. A categorical scale owns one
27
+ * of these beside its slots, and the shaders read it through the same id they
28
+ * already resolve to a slot, so every mark drawn against the scale dims its
29
+ * inactive categories without knowing whether it is the source of the
30
+ * selection, filtered by it, or outside the filter group.
31
+ *
32
+ * The same shape as SlotTransition, on its own clock: `from` and `to` are the
33
+ * two ends of the fade, the value NOW is `mix(from, to, t)`, the GPU computes it
34
+ * for every id, and `activityOf` for the few a CPU consumer asks about. A
35
+ * SEPARATE clock from the slots', because a click changes what is selected
36
+ * without changing any data, and a data update has nothing to do with dimming.
37
+ * One `t` for every id, not a spring per category: everything fading because of
38
+ * one click arrives together, and there is one moment of rest.
39
+ */
40
+ export declare class ActivityTable {
41
+ #private;
42
+ /**
43
+ * Bumped whenever `from` or `to` change: a selection, a resize, a settle. The
44
+ * packer compares this rather than the contents, so a frame where only `t`
45
+ * moved uploads nothing.
46
+ */
47
+ version: number;
48
+ readonly clock: TransitionClock;
49
+ /**
50
+ * `charts` is required: the loop redraws only the charts a clock names, so a
51
+ * fade whose clock named none would animate without anything redrawing it.
52
+ * `loop` is null in unit tests and before a coordinator exists, and then a
53
+ * fade lands at once. `onFrame` is called for every driven frame and once
54
+ * more at rest, for CPU consumers such as axis labels.
55
+ */
56
+ constructor(loop: ClockLoop | null, options: {
57
+ charts: () => string[];
58
+ onFrame?: () => void;
59
+ stiffness?: number;
60
+ });
61
+ /** How many ids the table covers. */
62
+ get count(): number;
63
+ get from(): Float32Array;
64
+ get to(): Float32Array;
65
+ /** Extrapolated for the frame being drawn. 1 when settled. */
66
+ get t(): number;
67
+ /** True while the clock is in the loop. */
68
+ get isAnimating(): boolean;
69
+ /**
70
+ * How active one id is now, 0..1. An id outside the table is fully active,
71
+ * the same answer the shader gives for an id past the block's count.
72
+ */
73
+ activityOf(id: number): number;
74
+ /**
75
+ * Cover `count` ids. Existing ids keep their fade; new ones snap to their
76
+ * state rather than fading in.
77
+ *
78
+ * `active` is the selection the new ids join, as ids: null for none, so
79
+ * every new id is active. Without it, a new id is inactive while something
80
+ * is selected, since a category that did not exist when the selection was
81
+ * made is not part of it. Passing it matters when the selection was made
82
+ * before the ids existed (before identity landed, or across a reload): the
83
+ * table has not seen it, and a new id it names must start selected rather
84
+ * than fade in from 0, and one it does not name must start dim rather than
85
+ * fade down from 1.
86
+ *
87
+ * `resize(0)` drops every id, for ids that stopped meaning their category.
88
+ *
89
+ * `trailing`: the last slot is not an id but a slot of its own that stays
90
+ * last (FieldActivity's free slot). Its fade moves to the new end, and the
91
+ * slot it leaves is a new id like any other, rather than inheriting it.
92
+ */
93
+ resize(count: number, active?: readonly number[] | null, trailing?: boolean): void;
94
+ /**
95
+ * Select exactly these ids; every other id fades out. Ids outside the table
96
+ * are ignored. Does nothing when the selection is the one already headed to.
97
+ */
98
+ setActive(ids: Iterable<number>): void;
99
+ /** Nothing selected: every id fades back to active. */
100
+ clear(): void;
101
+ /** Leave the loop, for an owner being torn down. Later changes land at once. */
102
+ destroy(): void;
103
+ }
@@ -0,0 +1,220 @@
1
+ import { TransitionClock } from '../animation';
2
+ /**
3
+ * Stiffness for a selection fade: ~0.25 s to settle, against ~0.32 s for a data
4
+ * transition (TRANSITION_STIFFNESS, 240). A click should feel immediate; the
5
+ * charts it filters take their own time to re-query and morph.
6
+ */
7
+ export const SELECTION_STIFFNESS = 400;
8
+ /**
9
+ * How visible an inactive category is, by default: marks are drawn at this
10
+ * opacity (the shaders' SELECTION_DIM_STRENGTH is 1 - this), and
11
+ * band labels fade to it.
12
+ */
13
+ export const DEFAULT_INACTIVE_OPACITY = 0.25;
14
+ /**
15
+ * How visible a category is for its activity: `inactiveOpacity` when
16
+ * unselected, 1 when selected, and in between mid-fade.
17
+ *
18
+ * The CPU twin of the shaders' `selectionDim`, which computes the same thing as
19
+ * `1 - dimStrength * (1 - activity)` with `dimStrength = 1 - inactiveOpacity`.
20
+ * Band labels and legend entries use this, so they match the marks.
21
+ */
22
+ export function selectionOpacity(activity, inactiveOpacity = DEFAULT_INACTIVE_OPACITY) {
23
+ return inactiveOpacity + (1 - inactiveOpacity) * activity;
24
+ }
25
+ /**
26
+ * Which categories of a scale are selected, and how far each one has faded.
27
+ *
28
+ * One float per identity id: 1 active, 0 inactive. A categorical scale owns one
29
+ * of these beside its slots, and the shaders read it through the same id they
30
+ * already resolve to a slot, so every mark drawn against the scale dims its
31
+ * inactive categories without knowing whether it is the source of the
32
+ * selection, filtered by it, or outside the filter group.
33
+ *
34
+ * The same shape as SlotTransition, on its own clock: `from` and `to` are the
35
+ * two ends of the fade, the value NOW is `mix(from, to, t)`, the GPU computes it
36
+ * for every id, and `activityOf` for the few a CPU consumer asks about. A
37
+ * SEPARATE clock from the slots', because a click changes what is selected
38
+ * without changing any data, and a data update has nothing to do with dimming.
39
+ * One `t` for every id, not a spring per category: everything fading because of
40
+ * one click arrives together, and there is one moment of rest.
41
+ */
42
+ export class ActivityTable {
43
+ #from = new Float32Array(0);
44
+ #to = new Float32Array(0);
45
+ /**
46
+ * Where a new selection is computed before it is compared with `to`. Reused,
47
+ * because `setActive` is called on every filter update, and most of those
48
+ * (every brush move on some other field) change nothing here.
49
+ */
50
+ #scratch = new Float32Array(0);
51
+ /** Whether some categories are selected, which decides what a new id starts as. */
52
+ #selecting = false;
53
+ /**
54
+ * Bumped whenever `from` or `to` change: a selection, a resize, a settle. The
55
+ * packer compares this rather than the contents, so a frame where only `t`
56
+ * moved uploads nothing.
57
+ */
58
+ version = 0;
59
+ clock;
60
+ #onFrame;
61
+ /**
62
+ * `charts` is required: the loop redraws only the charts a clock names, so a
63
+ * fade whose clock named none would animate without anything redrawing it.
64
+ * `loop` is null in unit tests and before a coordinator exists, and then a
65
+ * fade lands at once. `onFrame` is called for every driven frame and once
66
+ * more at rest, for CPU consumers such as axis labels.
67
+ */
68
+ constructor(loop, options) {
69
+ this.#onFrame = options.onFrame ?? null;
70
+ this.clock = new TransitionClock({
71
+ loop,
72
+ charts: options.charts,
73
+ stiffness: options.stiffness ?? SELECTION_STIFFNESS,
74
+ onRest: () => this.#onClockRest(),
75
+ onFrame: this.#onFrame ?? undefined
76
+ });
77
+ }
78
+ /** How many ids the table covers. */
79
+ get count() {
80
+ return this.#to.length;
81
+ }
82
+ get from() {
83
+ return this.#from;
84
+ }
85
+ get to() {
86
+ return this.#to;
87
+ }
88
+ /** Extrapolated for the frame being drawn. 1 when settled. */
89
+ get t() {
90
+ return this.clock.t;
91
+ }
92
+ /** True while the clock is in the loop. */
93
+ get isAnimating() {
94
+ return this.clock.running;
95
+ }
96
+ /**
97
+ * How active one id is now, 0..1. An id outside the table is fully active,
98
+ * the same answer the shader gives for an id past the block's count.
99
+ */
100
+ activityOf(id) {
101
+ const f = this.#from[id];
102
+ if (f === undefined)
103
+ return 1;
104
+ return f + (this.#to[id] - f) * this.clock.t;
105
+ }
106
+ /**
107
+ * Cover `count` ids. Existing ids keep their fade; new ones snap to their
108
+ * state rather than fading in.
109
+ *
110
+ * `active` is the selection the new ids join, as ids: null for none, so
111
+ * every new id is active. Without it, a new id is inactive while something
112
+ * is selected, since a category that did not exist when the selection was
113
+ * made is not part of it. Passing it matters when the selection was made
114
+ * before the ids existed (before identity landed, or across a reload): the
115
+ * table has not seen it, and a new id it names must start selected rather
116
+ * than fade in from 0, and one it does not name must start dim rather than
117
+ * fade down from 1.
118
+ *
119
+ * `resize(0)` drops every id, for ids that stopped meaning their category.
120
+ *
121
+ * `trailing`: the last slot is not an id but a slot of its own that stays
122
+ * last (FieldActivity's free slot). Its fade moves to the new end, and the
123
+ * slot it leaves is a new id like any other, rather than inheriting it.
124
+ */
125
+ resize(count, active, trailing = false) {
126
+ if (count === this.#to.length)
127
+ return;
128
+ const oldLength = this.#to.length;
129
+ const tail = trailing && oldLength > 0 && count > 0;
130
+ const keep = tail ? Math.min(oldLength - 1, count - 1) : Math.min(oldLength, count);
131
+ const ids = tail ? count - 1 : count;
132
+ const selecting = active === undefined ? this.#selecting : active !== null;
133
+ const fill = selecting ? 0 : 1;
134
+ const grown = (old) => {
135
+ const next = new Float32Array(count);
136
+ next.fill(fill);
137
+ next.set(old.subarray(0, keep), 0);
138
+ if (active) {
139
+ for (const id of active)
140
+ if (id >= keep && id < ids)
141
+ next[id] = 1;
142
+ }
143
+ if (tail)
144
+ next[count - 1] = old[oldLength - 1];
145
+ return next;
146
+ };
147
+ this.#from = grown(this.#from);
148
+ this.#to = grown(this.#to);
149
+ this.#scratch = new Float32Array(count);
150
+ this.version++;
151
+ }
152
+ /**
153
+ * Select exactly these ids; every other id fades out. Ids outside the table
154
+ * are ignored. Does nothing when the selection is the one already headed to.
155
+ */
156
+ setActive(ids) {
157
+ const next = this.#scratch;
158
+ next.fill(0);
159
+ for (const id of ids) {
160
+ if (id >= 0 && id < next.length)
161
+ next[id] = 1;
162
+ }
163
+ this.#selecting = true;
164
+ this.#retarget(next);
165
+ }
166
+ /** Nothing selected: every id fades back to active. */
167
+ clear() {
168
+ const next = this.#scratch;
169
+ next.fill(1);
170
+ this.#selecting = false;
171
+ this.#retarget(next);
172
+ }
173
+ /** Leave the loop, for an owner being torn down. Later changes land at once. */
174
+ destroy() {
175
+ this.clock.stop();
176
+ }
177
+ /**
178
+ * Fold, point every id at its new state, and wake the loop.
179
+ *
180
+ * Returns early when `next` equals `to`: the fade already in flight (or the
181
+ * rest already reached) is headed exactly there, and restarting it would
182
+ * re-fold and bump the version for nothing, on every filter update.
183
+ */
184
+ #retarget(next) {
185
+ const from = this.#from;
186
+ const to = this.#to;
187
+ let changed = false;
188
+ for (let id = 0; id < to.length; id++) {
189
+ if (to[id] !== next[id]) {
190
+ changed = true;
191
+ break;
192
+ }
193
+ }
194
+ if (!changed)
195
+ return;
196
+ // THE FOLD: every id continues from where it is now, so a click landing
197
+ // mid-fade reverses or redirects it in place instead of jumping.
198
+ const t = this.clock.t;
199
+ for (let id = 0; id < to.length; id++) {
200
+ from[id] += (to[id] - from[id]) * t;
201
+ to[id] = next[id];
202
+ }
203
+ this.clock.restart();
204
+ this.version++;
205
+ }
206
+ /**
207
+ * Land exactly on `to`. A spring rests at an epsilon, and a table left at
208
+ * 0.0003 would start its next fold from a value that was never a state.
209
+ */
210
+ #settle() {
211
+ this.clock.settle();
212
+ this.#from.set(this.#to);
213
+ }
214
+ #onClockRest() {
215
+ this.#settle();
216
+ this.version++;
217
+ // The last frame, at exactly t = 1, which the loop's final frame was not.
218
+ this.#onFrame?.();
219
+ }
220
+ }
@@ -1,5 +1,5 @@
1
1
  import type { ScaleBand } from 'd3-scale';
2
- import type { DimValue, FieldIdentity } from '../query-engine/categorical-identity';
2
+ import type { DimValue, FieldIdentity, SyntheticId } from '../query-engine/categorical-identity';
3
3
  import type { ClockLoop } from '../animation';
4
4
  import { type BandSlots } from './SlotTransition';
5
5
  /**
@@ -77,10 +77,6 @@ export declare class BandScaleState {
77
77
  currentDataPoint: number | null;
78
78
  constructor(name: string, domain?: DimValue[], config?: BandScaleConfig);
79
79
  setReady(): void;
80
- get activeValues(): Set<DimValue> | null;
81
- setActiveValues(values: Iterable<DimValue>): void;
82
- clearActiveValues(): void;
83
- isActive(value: DimValue): boolean;
84
80
  /**
85
81
  * Marks the scale dead. Clears nothing, deliberately.
86
82
  *
@@ -121,6 +117,12 @@ export declare class BandScaleState {
121
117
  * where name is "__facet__species" but sourceField is "species").
122
118
  */
123
119
  get sourceField(): string;
120
+ /**
121
+ * The identity key of the dim this scale adopted, or null before it has one.
122
+ * Selection activity is kept per identity key, so this is what the packer
123
+ * points the scale's block at.
124
+ */
125
+ get identityKey(): string | null;
124
126
  /**
125
127
  * Enable or disable scale updates
126
128
  * When disabled, domain is frozen at current state
@@ -209,6 +211,26 @@ export declare class BandScaleState {
209
211
  * undisplayed Other is ignored.
210
212
  */
211
213
  setOtherId(id: number | null): void;
214
+ /** This axis's Other bucket id, while a mark on it has one; null otherwise. */
215
+ get otherId(): number | null;
216
+ /**
217
+ * This axis's Other bucket, shared by every mark on it that has one.
218
+ *
219
+ * One id per band scale, not per mark or per chart: marks share a band scale
220
+ * only when they agree on what Other is — the scale's key carries the table
221
+ * and the category-reducing transform (its `transformKey`), N included — so
222
+ * everything drawn on one axis lines up in one Other band. A bar and its
223
+ * value labels, or two charts with the identical topN, share it.
224
+ *
225
+ * Reference-counted by mark id. The first holder acquires; holders arriving
226
+ * while that is in flight share it; the last to release gives it back.
227
+ */
228
+ retainOther(holder: string, acquire: () => Promise<SyntheticId>): Promise<SyntheticId>;
229
+ /**
230
+ * Drop `holder`'s claim on the Other bucket. The last one out hands the id to
231
+ * `release` — once the acquire has landed, if it is still in flight.
232
+ */
233
+ releaseOther(holder: string, release: (other: SyntheticId) => void): void;
212
234
  /**
213
235
  * Identity id currently displayed at `slot`, or null when the slot holds no
214
236
  * category. Use this to compare a display position — what invertScreenToData
@@ -311,7 +333,7 @@ export declare class BandScaleState {
311
333
  /**
312
334
  * Get D3 band scale configured for rendering
313
335
  */
314
- getBaseScale(axis: string, innerWidthOrHeight: number): ScaleBand<DimValue>;
336
+ getBaseScale(_axis: string, innerWidthOrHeight: number): ScaleBand<DimValue>;
315
337
  /**
316
338
  * Update scale domain (for D3 scale)
317
339
  */
@@ -375,26 +397,30 @@ export declare class BandScaleState {
375
397
  * so it fades out over the half slot before that centre reaches the axis end
376
398
  * (the animated count), and is gone from there; the same at the start. An
377
399
  * entrant's label appears as its band crosses in.
400
+ *
401
+ * A selection's dimming is not here: it belongs to the filter groups of the
402
+ * marks the axis labels, so the axis multiplies it in (BandAxisTicks).
378
403
  */
379
404
  tickOpacity(key: number): number;
380
405
  /**
381
406
  * Where a tick's label sits now, in pixels along the axis: the centre of its
382
407
  * category's current slot, so it travels with the bar. Slot 0 is at the start
383
- * of the range on x and at the bottom on y, as in the shader and d3's
384
- * reversed range. Per frame, O(1).
408
+ * of the range on both axes: the left on x, the top on y, as in the shader
409
+ * and d3's range. Per frame, O(1).
385
410
  */
386
- tickPosition(key: number, rangeSize: number, axis: 'x' | 'y'): number;
411
+ tickPosition(key: number, rangeSize: number): number;
387
412
  /**
388
413
  * The same, for a caller that already has the slot.
389
414
  *
390
415
  * A consumer that has to test the slot first — an SVG label, which must not
391
416
  * draw a category the axis is not displaying — would otherwise ask for it
392
- * twice per item per frame. The axis flip lives here rather than being
393
- * re-derived by such a caller.
417
+ * twice per item per frame.
418
+ *
419
+ * The same on both axes. A y band axis reads top-down, like a list: slot 0
420
+ * at the top, so a ranking and an explicit domain need no reversing, and
421
+ * the exit one past the last slot is the end of the list on either axis.
394
422
  */
395
- slotPosition(slot: number, rangeSize: number, axis: 'x' | 'y'): number;
396
- /** Whether a tick's category is outside the current selection. */
397
- isTickDimmed(tick: BandTick): boolean;
423
+ slotPosition(slot: number, rangeSize: number): number;
398
424
  /**
399
425
  * Centre of a (possibly fractional) slot, measured from the start of the
400
426
  * range — the same layout the shader's computeBandLayout uses, so a label
@@ -431,7 +457,7 @@ export declare class BandScaleState {
431
457
  * Returns the 0-based index of the band under the cursor, or null if
432
458
  * no bands exist.
433
459
  */
434
- invertScreenToData(screenVal: number, rangeSize: number, isYAxis?: boolean): number | null;
460
+ invertScreenToData(screenVal: number, rangeSize: number, _isYAxis?: boolean): number | null;
435
461
  /**
436
462
  * Get all mark extents (for debugging). Ids when the scale has identity.
437
463
  */