@fjader/svelte 0.2.2 → 0.2.3

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 (118) hide show
  1. package/dist/components/axes/BandAxisTicks.svelte +63 -24
  2. package/dist/components/axes/BandAxisTicks.svelte.d.ts +5 -5
  3. package/dist/components/charts/ChartContainer.svelte +2 -1
  4. package/dist/components/guides/legend/ContinuousColorLegend.svelte +2 -1
  5. package/dist/components/guides/legend/OrdinalColorLegend.svelte +92 -34
  6. package/dist/components/marks/AreaMarkX.svelte +19 -6
  7. package/dist/components/marks/AreaMarkY.svelte +19 -6
  8. package/dist/components/marks/BarMarkX.svelte +3 -0
  9. package/dist/components/marks/BarMarkY.svelte +3 -0
  10. package/dist/components/marks/DotsMark.svelte +3 -0
  11. package/dist/components/marks/LineMark.svelte +3 -0
  12. package/dist/components/marks/ParallelCoordinatesMark.svelte +6 -0
  13. package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +3 -0
  14. package/dist/components/marks/RectMark.svelte +3 -0
  15. package/dist/components/marks/RectXMark.svelte +3 -0
  16. package/dist/components/marks/RectYMark.svelte +3 -0
  17. package/dist/components/marks/RuleMarkX.svelte +10 -2
  18. package/dist/components/marks/RuleMarkY.svelte +10 -2
  19. package/dist/components/marks/TextMark.svelte +68 -4
  20. package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +15 -1
  21. package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +116 -27
  22. package/dist/components/marks/lifecycle/domain-queries.js +13 -11
  23. package/dist/components/marks/lifecycle/transform-scale-key.d.ts +16 -0
  24. package/dist/components/marks/lifecycle/transform-scale-key.js +27 -0
  25. package/dist/components/marks/utils/area-outline.d.ts +24 -0
  26. package/dist/components/marks/utils/area-outline.js +10 -0
  27. package/dist/components/marks/utils/reactive-config.d.ts +1 -1
  28. package/dist/components/marks/utils/reactive-config.js +3 -0
  29. package/dist/components/rendering/Areas.svelte +30 -6
  30. package/dist/components/rendering/Areas.svelte.d.ts +2 -0
  31. package/dist/components/rendering/BaseGPURenderer.svelte.js +4 -1
  32. package/dist/components/rendering/Dots.svelte +24 -3
  33. package/dist/components/rendering/Dots.svelte.d.ts +2 -0
  34. package/dist/components/rendering/GridLine.svelte +2 -1
  35. package/dist/components/rendering/Lines.svelte +49 -7
  36. package/dist/components/rendering/Lines.svelte.d.ts +9 -0
  37. package/dist/components/rendering/ParallelLines.svelte +26 -7
  38. package/dist/components/rendering/ParallelLines.svelte.d.ts +2 -0
  39. package/dist/components/rendering/Rects.svelte +20 -5
  40. package/dist/components/rendering/Rects.svelte.d.ts +2 -0
  41. package/dist/components/rendering/RuleX.svelte +20 -5
  42. package/dist/components/rendering/RuleX.svelte.d.ts +2 -0
  43. package/dist/components/rendering/RuleY.svelte +20 -5
  44. package/dist/components/rendering/RuleY.svelte.d.ts +2 -0
  45. package/dist/components/rendering/activity-reads.d.ts +93 -0
  46. package/dist/components/rendering/activity-reads.js +135 -0
  47. package/dist/components/rendering/uniform-sizes.d.ts +26 -0
  48. package/dist/components/rendering/uniform-sizes.js +28 -0
  49. package/dist/config/ChartConfig.svelte.d.ts +2 -0
  50. package/dist/gpu/ColorScaleShaderLib.d.ts +13 -1
  51. package/dist/gpu/ColorScaleShaderLib.js +56 -5
  52. package/dist/gpu/GPUCoordinator.svelte.d.ts +8 -0
  53. package/dist/gpu/GPUCoordinator.svelte.js +68 -32
  54. package/dist/gpu/ResolveShader.d.ts +2 -2
  55. package/dist/gpu/ResolveShader.js +2 -2
  56. package/dist/gpu/ScaleTemplateSystem.d.ts +19 -0
  57. package/dist/gpu/ScaleTemplateSystem.js +86 -7
  58. package/dist/gpu/SizeScaleShaderLib.d.ts +4 -4
  59. package/dist/gpu/SizeScaleShaderLib.js +4 -4
  60. package/dist/gpu/identity-slot-packing.d.ts +157 -40
  61. package/dist/gpu/identity-slot-packing.js +262 -62
  62. package/dist/gpu/tracked-scales.d.ts +14 -0
  63. package/dist/gpu/tracked-scales.js +18 -0
  64. package/dist/index.js +1 -0
  65. package/dist/interactors/BandLabelToggle.svelte +5 -9
  66. package/dist/interactors/BandLabelToggle.svelte.d.ts +3 -3
  67. package/dist/interactors/DerivedInterval1D.svelte +26 -54
  68. package/dist/interactors/Interval1D.svelte +2 -1
  69. package/dist/interactors/Interval2D.svelte +2 -1
  70. package/dist/interactors/ParallelAxisSource.svelte +4 -4
  71. package/dist/interactors/ParallelAxisSource.svelte.d.ts +2 -1
  72. package/dist/interactors/ParallelBrushes.svelte +8 -9
  73. package/dist/interactors/ParallelBrushes.svelte.d.ts +3 -3
  74. package/dist/interactors/Toggle.svelte +17 -36
  75. package/dist/interactors/Toggle.svelte.d.ts +4 -3
  76. package/dist/interactors/Tooltip.svelte +2 -1
  77. package/dist/interactors/TooltipManager.svelte.js +8 -3
  78. package/dist/interactors/relay-clauses.d.ts +23 -0
  79. package/dist/interactors/relay-clauses.js +60 -0
  80. package/dist/query-engine/CrossfilterSelection.svelte.d.ts +88 -15
  81. package/dist/query-engine/CrossfilterSelection.svelte.js +94 -20
  82. package/dist/query-engine/FieldActivity.svelte.d.ts +79 -0
  83. package/dist/query-engine/FieldActivity.svelte.js +207 -0
  84. package/dist/query-engine/FilterGroup.svelte.d.ts +53 -29
  85. package/dist/query-engine/FilterGroup.svelte.js +181 -122
  86. package/dist/query-engine/FilterGroupRegistry.svelte.d.ts +42 -0
  87. package/dist/query-engine/FilterGroupRegistry.svelte.js +103 -1
  88. package/dist/query-engine/MarkHandle.svelte.d.ts +9 -1
  89. package/dist/query-engine/MarkHandle.svelte.js +12 -2
  90. package/dist/query-engine/QueryEngine.d.ts +52 -16
  91. package/dist/query-engine/QueryEngine.js +90 -28
  92. package/dist/query-engine/categorical-identity.d.ts +99 -27
  93. package/dist/query-engine/categorical-identity.js +210 -51
  94. package/dist/query-engine/index.d.ts +2 -1
  95. package/dist/query-engine/index.js +1 -0
  96. package/dist/query-engine/preagg/Preagg.d.ts +1 -1
  97. package/dist/query-engine/preagg/Preagg.js +11 -9
  98. package/dist/query-engine/preagg/plan.d.ts +9 -5
  99. package/dist/query-engine/preagg/plan.js +13 -11
  100. package/dist/query-engine/toggle-predicate.js +12 -1
  101. package/dist/scales/ActivityTable.d.ts +99 -0
  102. package/dist/scales/ActivityTable.js +211 -0
  103. package/dist/scales/BandScaleState.svelte.d.ts +28 -7
  104. package/dist/scales/BandScaleState.svelte.js +55 -20
  105. package/dist/scales/ChartScales.svelte.d.ts +2 -2
  106. package/dist/scales/ColorScaleState.svelte.d.ts +36 -8
  107. package/dist/scales/ColorScaleState.svelte.js +118 -24
  108. package/dist/scales/DomainResolver.d.ts +2 -0
  109. package/dist/scales/DomainResolver.js +15 -8
  110. package/dist/scales/ScaleRegistry.svelte.d.ts +9 -0
  111. package/dist/scales/ScaleRegistry.svelte.js +22 -1
  112. package/dist/scales/TimeScaleState.svelte.js +7 -1
  113. package/dist/spec/index.js +1 -0
  114. package/dist/spec/types.d.ts +2 -0
  115. package/dist/transforms/index.d.ts +27 -0
  116. package/dist/transforms/index.js +97 -3
  117. package/dist/types.d.ts +30 -0
  118. package/package.json +1 -1
@@ -3,8 +3,9 @@
3
3
  * mark registration for batch filter-driven re-queries.
4
4
  *
5
5
  * The mark layer's only interface to DuckDB. All query execution goes through
6
- * queryEngine.query(sql, opts) for ad-hoc queries, or registerMark() +
7
- * onFilterChange() for the reactive registration path.
6
+ * queryEngine.query(sql, opts) for ad-hoc queries, or registerMark() for the
7
+ * reactive registration path: FilterGroup calls onClauseChange() when one
8
+ * clause changes, and onFilterChange() when all of a group's do (clearAll).
8
9
  *
9
10
  * v1: Direct execution (await connection.query(sql))
10
11
  * v2: Cancel-on-supersede — keyed on callerId, drops stale in-flight queries.
@@ -22,8 +23,9 @@ import { type QueryParts } from '../components/marks/lifecycle/query-builder';
22
23
  import type { ActivationSource } from './preagg/plan';
23
24
  export type { ActiveDim, ActivationSource } from './preagg/plan';
24
25
  import { MarkHandle } from './MarkHandle.svelte';
25
- import { type DimValue, type FieldIdentity, type IdentityField } from './categorical-identity';
26
+ import { type DimValue, type FieldIdentity, type IdentityField, type SyntheticId } from './categorical-identity';
26
27
  import type { FilterGroup } from './FilterGroup.svelte';
28
+ import { type Clause, type MarkRoute } from './CrossfilterSelection.svelte';
27
29
  import type { TransformCompiler } from '../transforms/TransformCompiler';
28
30
  export interface QueryOpts {
29
31
  /** Caller identifier for cancel-on-supersede. When a new query arrives
@@ -82,6 +84,8 @@ export interface QueryDescriptor {
82
84
  parts: QueryParts;
83
85
  schema: Map<string, ColumnSchema>;
84
86
  transformCompiler: TransformCompiler;
87
+ /** What routing needs to know about the mark. Absent in tests that route nothing. */
88
+ route?: MarkRoute;
85
89
  }
86
90
  export declare class QueryEngine {
87
91
  #private;
@@ -127,27 +131,40 @@ export declare class QueryEngine {
127
131
  */
128
132
  dimTableFor(field: string): string;
129
133
  /**
130
- * Dim values for `field`, indexed by id — the decode side of the string -> id
131
- * encode. Not display labels: formatting belongs to the scale/axis layer.
134
+ * The field's categories as of now, in id order, without the synthetic Other
135
+ * buckets: a bucket is not a category of the data, and another chart's bucket
136
+ * must not appear on an axis, a facet or a colour legend that has none.
132
137
  *
133
- * The LIVE array — hold the reference, do not snapshot it. A second table
134
- * registering the same field appends to the dim afterwards and this array
135
- * grows in place.
138
+ * With `table`, only the categories that table holds: the dim spans every
139
+ * table registering the field, and an axis over one table must not show a
140
+ * slot for a category only another has. Without it (a scale shared per field,
141
+ * like ordinal colour), every category of the dim.
142
+ *
143
+ * A snapshot, not the live array: for decoding ids, hold `identityFor(field)`.
144
+ */
145
+ categoryValues(field: string, table?: string): DimValue[];
146
+ /**
147
+ * The table of ids `table` holds for `field`, for `dense()` to pad from, or
148
+ * null before the table has folded into the field's dim.
136
149
  */
137
- identityValues(field: string): DimValue[];
150
+ presentTableFor(field: string, table: string): string | null;
138
151
  /**
139
152
  * The field's id <-> value bijection. One live object owning both
140
153
  * directions, so a holder cannot end up with a value array and a reverse map
141
- * that disagree. Prefer this over identityValues() anywhere the reverse
154
+ * that disagree. Prefer this over categoryValues() anywhere the reverse
142
155
  * direction is needed; do not rebuild it locally.
143
156
  */
144
157
  identityFor(field: string): FieldIdentity;
158
+ /** The field's current bijection, or null; never creates one (see CategoricalIdentity). */
159
+ peekIdentity(field: string): FieldIdentity | null;
145
160
  /**
146
- * Reserve an id for a value present in no table — the synthetic "Other"
147
- * bucket from `topN({ includeOthers })`. Call after ensureColumns.
148
- * Returns an existing id when a real category already carries the label.
161
+ * Acquire an id for a value present in no table — the synthetic "Other"
162
+ * bucket from `topN({ includeOthers })` — with the label it displays. Call
163
+ * after ensureColumns; release it when its holder goes.
149
164
  */
150
- reserveSyntheticIdentity(field: string, label: string): Promise<number>;
165
+ acquireSyntheticIdentity(field: string, label: string): Promise<SyntheticId>;
166
+ /** Give back an id from `acquireSyntheticIdentity`, for the next acquire to reuse. */
167
+ releaseSyntheticIdentity(synthetic: SyntheticId): void;
151
168
  /**
152
169
  * Subscribe to identity resets. Returns an unsubscribe function.
153
170
  *
@@ -234,10 +251,29 @@ export declare class QueryEngine {
234
251
  /** When a filter last changed, in any group, for settling (performance.now()). */
235
252
  get lastFilterChangeAt(): number;
236
253
  /**
237
- * Called by FilterGroup when any filter clause changes.
238
- * Iterates registered marks for this group and batch-queries them.
254
+ * Called by FilterGroup when its clauses change wholesale (clearAll):
255
+ * re-queries every mark in the group.
239
256
  */
240
257
  onFilterChange(group: FilterGroup): void;
258
+ /**
259
+ * Called by FilterGroup when the clause of `sourceId` changes: `prev` is the
260
+ * clause it replaced or removed, `next` the one now in force (null when
261
+ * removed).
262
+ *
263
+ * Re-queries only the group's marks that either one filters. A mark neither
264
+ * filters has the same SQL as before, so querying it would only assemble that
265
+ * SQL to find the cached result, on every brush frame. A mark that reads the
266
+ * clause's categorical field dims by it instead, and a mark in a chart that
267
+ * skips the source is not filtered by it: neither is re-queried.
268
+ */
269
+ onClauseChange(group: FilterGroup, sourceId: string, prev: Clause | undefined, next: Clause | null): void;
270
+ /**
271
+ * Called by FilterGroup when a registration that skips `chart` comes or
272
+ * goes while its source has a clause: re-queries the group's marks in that
273
+ * chart, whose filters may have changed. A mark whose SQL did not change
274
+ * stops at the cache and the same-Table guard.
275
+ */
276
+ onChartRouteChange(group: FilterGroup, chart: object): void;
241
277
  /**
242
278
  * Pre-aggregate marks for a filter group before a source is used: builds a
243
279
  * preagg table per mark the source filters (see Preagg.activate). Called
@@ -3,8 +3,9 @@
3
3
  * mark registration for batch filter-driven re-queries.
4
4
  *
5
5
  * The mark layer's only interface to DuckDB. All query execution goes through
6
- * queryEngine.query(sql, opts) for ad-hoc queries, or registerMark() +
7
- * onFilterChange() for the reactive registration path.
6
+ * queryEngine.query(sql, opts) for ad-hoc queries, or registerMark() for the
7
+ * reactive registration path: FilterGroup calls onClauseChange() when one
8
+ * clause changes, and onFilterChange() when all of a group's do (clearAll).
8
9
  *
9
10
  * v1: Direct execution (await connection.query(sql))
10
11
  * v2: Cancel-on-supersede — keyed on callerId, drops stale in-flight queries.
@@ -21,6 +22,7 @@ import { assembleQuery, castNumeric, splitWindowSelect } from '../components/mar
21
22
  import { Preagg } from './preagg/Preagg';
22
23
  import { MarkHandle } from './MarkHandle.svelte';
23
24
  import { CategoricalIdentity } from './categorical-identity';
25
+ import { NO_ROUTE } from './CrossfilterSelection.svelte';
24
26
  /**
25
27
  * How long a queued build waits behind mark queries before it is sent anyway,
26
28
  * by default. During a drag on the base table a mark query is always waiting
@@ -197,32 +199,64 @@ export class QueryEngine {
197
199
  return this.#identity.dimTable(field);
198
200
  }
199
201
  /**
200
- * Dim values for `field`, indexed by id — the decode side of the string -> id
201
- * encode. Not display labels: formatting belongs to the scale/axis layer.
202
+ * The field's categories as of now, in id order, without the synthetic Other
203
+ * buckets: a bucket is not a category of the data, and another chart's bucket
204
+ * must not appear on an axis, a facet or a colour legend that has none.
202
205
  *
203
- * The LIVE array — hold the reference, do not snapshot it. A second table
204
- * registering the same field appends to the dim afterwards and this array
205
- * grows in place.
206
+ * With `table`, only the categories that table holds: the dim spans every
207
+ * table registering the field, and an axis over one table must not show a
208
+ * slot for a category only another has. Without it (a scale shared per field,
209
+ * like ordinal colour), every category of the dim.
210
+ *
211
+ * A snapshot, not the live array: for decoding ids, hold `identityFor(field)`.
206
212
  */
207
- identityValues(field) {
208
- return this.#identity.values(field);
213
+ categoryValues(field, table) {
214
+ const identity = this.#identity.identity(field);
215
+ const values = identity.values;
216
+ const present = table ? this.#identity.presentIds(field, table) : null;
217
+ if (identity.synthetic.size === 0 && !present)
218
+ return values.slice();
219
+ const out = [];
220
+ for (let id = 0; id < values.length; id++) {
221
+ if (identity.synthetic.has(id))
222
+ continue;
223
+ if (present && !present.has(id))
224
+ continue;
225
+ out.push(values[id]);
226
+ }
227
+ return out;
228
+ }
229
+ /**
230
+ * The table of ids `table` holds for `field`, for `dense()` to pad from, or
231
+ * null before the table has folded into the field's dim.
232
+ */
233
+ presentTableFor(field, table) {
234
+ return this.#identity.presentTable(field, table);
209
235
  }
210
236
  /**
211
237
  * The field's id <-> value bijection. One live object owning both
212
238
  * directions, so a holder cannot end up with a value array and a reverse map
213
- * that disagree. Prefer this over identityValues() anywhere the reverse
239
+ * that disagree. Prefer this over categoryValues() anywhere the reverse
214
240
  * direction is needed; do not rebuild it locally.
215
241
  */
216
242
  identityFor(field) {
217
243
  return this.#identity.identity(field);
218
244
  }
245
+ /** The field's current bijection, or null; never creates one (see CategoricalIdentity). */
246
+ peekIdentity(field) {
247
+ return this.#identity.peekIdentity(field);
248
+ }
219
249
  /**
220
- * Reserve an id for a value present in no table — the synthetic "Other"
221
- * bucket from `topN({ includeOthers })`. Call after ensureColumns.
222
- * Returns an existing id when a real category already carries the label.
250
+ * Acquire an id for a value present in no table — the synthetic "Other"
251
+ * bucket from `topN({ includeOthers })` — with the label it displays. Call
252
+ * after ensureColumns; release it when its holder goes.
223
253
  */
224
- reserveSyntheticIdentity(field, label) {
225
- return this.#identity.reserveSynthetic(field, label);
254
+ acquireSyntheticIdentity(field, label) {
255
+ return this.#identity.acquireSynthetic(field, label);
256
+ }
257
+ /** Give back an id from `acquireSyntheticIdentity`, for the next acquire to reuse. */
258
+ releaseSyntheticIdentity(synthetic) {
259
+ this.#identity.releaseSynthetic(synthetic);
226
260
  }
227
261
  /**
228
262
  * Subscribe to identity resets. Returns an unsubscribe function.
@@ -559,27 +593,55 @@ export class QueryEngine {
559
593
  return this.#lastFilterChangeAt;
560
594
  }
561
595
  /**
562
- * Called by FilterGroup when any filter clause changes.
563
- * Iterates registered marks for this group and batch-queries them.
596
+ * Called by FilterGroup when its clauses change wholesale (clearAll):
597
+ * re-queries every mark in the group.
564
598
  */
565
599
  onFilterChange(group) {
566
600
  this.#lastFilterChangeAt = performance.now();
567
- const batch = [];
568
601
  for (const [markId, reg] of this.#marks) {
569
- if (reg.filterGroup === group) {
570
- batch.push([markId, reg]);
571
- }
602
+ if (reg.filterGroup === group)
603
+ this.#queryMark(markId, reg);
572
604
  }
573
- if (batch.length > 0) {
574
- this.#executeBatch(batch);
605
+ }
606
+ /**
607
+ * Called by FilterGroup when the clause of `sourceId` changes: `prev` is the
608
+ * clause it replaced or removed, `next` the one now in force (null when
609
+ * removed).
610
+ *
611
+ * Re-queries only the group's marks that either one filters. A mark neither
612
+ * filters has the same SQL as before, so querying it would only assemble that
613
+ * SQL to find the cached result, on every brush frame. A mark that reads the
614
+ * clause's categorical field dims by it instead, and a mark in a chart that
615
+ * skips the source is not filtered by it: neither is re-queried.
616
+ */
617
+ onClauseChange(group, sourceId, prev, next) {
618
+ this.#lastFilterChangeAt = performance.now();
619
+ const { selection } = group;
620
+ for (const [markId, reg] of this.#marks) {
621
+ if (reg.filterGroup !== group)
622
+ continue;
623
+ const route = reg.descriptor.route ?? NO_ROUTE;
624
+ if ((prev && selection.applies(sourceId, prev, route)) ||
625
+ (next && selection.applies(sourceId, next, route))) {
626
+ this.#queryMark(markId, reg);
627
+ }
575
628
  }
576
629
  }
577
- #executeBatch(batch) {
578
- for (const [markId, reg] of batch)
579
- this.#queryMark(markId, reg);
630
+ /**
631
+ * Called by FilterGroup when a registration that skips `chart` comes or
632
+ * goes while its source has a clause: re-queries the group's marks in that
633
+ * chart, whose filters may have changed. A mark whose SQL did not change
634
+ * stops at the cache and the same-Table guard.
635
+ */
636
+ onChartRouteChange(group, chart) {
637
+ for (const [markId, reg] of this.#marks) {
638
+ if (reg.filterGroup === group && reg.descriptor.route?.chart === chart) {
639
+ this.#queryMark(markId, reg);
640
+ }
641
+ }
580
642
  }
581
643
  /**
582
- * Query a single mark: on a filter change (via #executeBatch) or a params
644
+ * Query a single mark: on a filter change or a params
583
645
  * change (via _onParamsChange). Reads a preagg table when one answers; if
584
646
  * that read fails, the table is set aside and the base table is queried once.
585
647
  */
@@ -611,7 +673,7 @@ export class QueryEngine {
611
673
  /** The query a mark runs on its base table, applying every clause. */
612
674
  #baseSQL(markId, reg) {
613
675
  const { descriptor, handle } = reg;
614
- return this.#assembleSQL(descriptor, descriptor.table, reg.filterGroup.selection.predicate(markId), handle.params);
676
+ return this.#assembleSQL(descriptor, descriptor.table, reg.filterGroup.selection.predicate(reg.descriptor.route ?? NO_ROUTE), handle.params);
615
677
  }
616
678
  /**
617
679
  * Assemble SQL from a query descriptor + filter + per-mark params.
@@ -11,14 +11,16 @@
11
11
  * later tables APPEND unseen values with continuing ids. It never reorders and
12
12
  * never renumbers, so an id already baked into a vertex buffer stays valid and
13
13
  * no earlier mark has to requery. `value` carries the source column's own type
14
- * and `label` is NULL, except on a synthetic row (`reserveSynthetic`), where
15
- * that is inverted.
14
+ * and `label` is NULL, except on a synthetic row (`acquireSynthetic`), where
15
+ * that is inverted, and on the NULL category's row, where both are NULL.
16
16
  *
17
17
  * 2. A **per-table view**, `<table>_v_s<session>`, selecting `*` plus one
18
18
  * `__dplot_<field>_idx` column per requested field. Always LEFT JOIN with
19
19
  * COALESCE(-1): an inner join silently drops rows whose value is not in the
20
- * dim (NULLs, values added later), which is data loss presenting as a
21
- * rendering bug. -1 is the unmapped sentinel, unified in `1ac4fd4`.
20
+ * dim (a value a later append will add), which is data loss presenting as a
21
+ * rendering bug. -1 is the unmapped sentinel, unified in `1ac4fd4`. A NULL
22
+ * value is not unmapped: the dim has a row for it (value NULL, label NULL),
23
+ * joined with IS NOT DISTINCT FROM, so NULL is a category like any other.
22
24
  *
23
25
  * 3. An **additive coalescer**, `ensureColumns()`. Concurrent callers union into
24
26
  * a pending set, one CREATE OR REPLACE VIEW runs with the full union, and a
@@ -41,6 +43,18 @@
41
43
  * the series scale all read the same columns from the same dim, and none of
42
44
  * them is special-cased here.
43
45
  */
46
+ /**
47
+ * A synthetic id handed out by `acquireSynthetic`: what to release it with.
48
+ * `generation` ties it to the dims it was acquired from, so a release after a
49
+ * reset does not offer a dropped row to the next acquire.
50
+ */
51
+ export interface SyntheticId {
52
+ readonly field: string;
53
+ readonly id: number;
54
+ /** The label it displays: the requested one, or `"<label> (rest)"` beside a real one. */
55
+ readonly label: string;
56
+ readonly generation: number;
57
+ }
44
58
  /** The subset of QueryEngine this module needs. Keeps it testable in isolation. */
45
59
  export interface IdentityExec {
46
60
  exec(sql: string | string[]): Promise<void>;
@@ -89,7 +103,15 @@ export interface IdentityField {
89
103
  * (SameValueZero, which covers bigint). That is why the round trip is correct
90
104
  * today; it is the declared types that were wrong.
91
105
  */
92
- export type DimValue = string | number | bigint;
106
+ export type DimValue = string | number | bigint | null;
107
+ /**
108
+ * How a category value is shown: as itself, except the NULL category, which has
109
+ * no text of its own. The one formatter for axis ticks, legend entries, facet
110
+ * labels and tooltips, so NULL reads the same everywhere. Selections keep the
111
+ * raw `null`: this is display only.
112
+ */
113
+ export declare const NULL_CATEGORY_LABEL = "(null)";
114
+ export declare function categoryLabel<T>(value: T): T | string;
93
115
  /**
94
116
  * A field's id <-> value bijection, both directions in one object.
95
117
  *
@@ -105,11 +127,32 @@ export type DimValue = string | number | bigint;
105
127
  * once and stay correct when a second table appends to the dim.
106
128
  */
107
129
  export declare class FieldIdentity {
108
- /** id -> source value. Dense and 0-based, so position is the id. */
130
+ /**
131
+ * id -> source value. Dense and 0-based, so position is the id. A synthetic
132
+ * id (an Other bucket) holds its display label here.
133
+ */
109
134
  readonly values: DimValue[];
110
- /** source value -> id. The exact inverse of `values`. */
135
+ /**
136
+ * source value -> id, for real values only: the inverse of `values` with the
137
+ * synthetic ids left out. A label is not a value, and several synthetic ids
138
+ * may share one, so nothing resolves a bucket by its label.
139
+ */
111
140
  readonly ids: Map<DimValue, number>;
112
- constructor(values?: readonly DimValue[]);
141
+ /** The synthetic ids (Other buckets): rows of the dim no source row joins to. */
142
+ readonly synthetic: Set<number>;
143
+ /**
144
+ * The NULL category's id, or null while the data has no NULL. An ordinary
145
+ * category: ranked, drawn, selectable; `values[nullId]` is `null`, and `ids`
146
+ * maps `null` to it, so value lookups need no special case.
147
+ */
148
+ nullId: number | null;
149
+ /**
150
+ * The identity key this is the bijection for, as `identityKeyFor` derives
151
+ * it: what a scale that adopted it is keyed by for selection activity.
152
+ * Empty for one built outside the engine, as tests do.
153
+ */
154
+ readonly key: string;
155
+ constructor(values?: readonly DimValue[], key?: string);
113
156
  /**
114
157
  * Bumped by `reset`, so a holder of this LIVE object can tell whether it
115
158
  * moved since it last looked. Appends, a same-length replacement and an
@@ -117,16 +160,21 @@ export declare class FieldIdentity {
117
160
  * comparing `values.length` is not.
118
161
  */
119
162
  version: number;
120
- /** Replace both directions. The only writer. */
121
- reset(values: readonly DimValue[]): void;
163
+ /**
164
+ * Replace both directions. The only writer. `synthetic` names the ids that
165
+ * are buckets rather than values: they keep their label in `values` and stay
166
+ * out of `ids`.
167
+ */
168
+ reset(values: readonly DimValue[], synthetic?: Iterable<number>): void;
122
169
  /** True once the dim has been built and holds at least one value. */
123
170
  get present(): boolean;
124
171
  }
125
172
  /**
126
- * Above this many distinct values a band axis is refused outright.
127
- * The shared slot buffer holds 16384 floats across every band and point scale,
128
- * so a single axis this large cannot render whatever else is on the page — the
129
- * limit is capacity, not taste.
173
+ * Above this many distinct values a band axis is refused outright. The shared
174
+ * slot buffer (IDENTITY_SLOT_CAPACITY floats) holds every band and point
175
+ * scale's slots and every selected field's activity, one run per filter group,
176
+ * so an axis this large would crowd out the rest of the page — the limit is
177
+ * capacity, not taste.
130
178
  */
131
179
  export declare const MAX_IDENTITY_CARDINALITY = 10000;
132
180
  /**
@@ -148,9 +196,9 @@ export declare class CategoricalIdentity {
148
196
  /** SQL producing `field`'s value, once a mark has registered it; undefined before. */
149
197
  sqlFor(field: string): string | undefined;
150
198
  /**
151
- * Whether `field`'s dim has folded in `table`, so every non-NULL value the
152
- * table holds has an id and `-1` can only mean NULL. A dim another table
153
- * built may lack this table's values; a refused dim covers nothing.
199
+ * Whether `field`'s dim has folded in `table`, so every value the table holds,
200
+ * NULL included, has an id and `-1` never appears. A dim another table built
201
+ * may lack this table's values; a refused dim covers nothing.
154
202
  */
155
203
  dimCovers(field: string, table: string): boolean;
156
204
  /** The field whose id column is `column`, or null if no dim has that column. */
@@ -159,6 +207,14 @@ export declare class CategoricalIdentity {
159
207
  viewHas(table: string, field: string): boolean;
160
208
  /** Name of the dim table for `field`. */
161
209
  dimTable(field: string): string;
210
+ /**
211
+ * The table of ids `table` holds for `field`, or null before that table has
212
+ * folded into the dim. What `dense()` pads from, so a chart never pads a
213
+ * category only another table has.
214
+ */
215
+ presentTable(field: string, table: string): string | null;
216
+ /** The ids `table` holds for `field`, or null before that table has folded in. */
217
+ presentIds(field: string, table: string): ReadonlySet<number> | null;
162
218
  /** Name of the view that carries id columns for `table`. */
163
219
  viewTable(table: string): string;
164
220
  /**
@@ -188,6 +244,12 @@ export declare class CategoricalIdentity {
188
244
  * it before the dim exists.
189
245
  */
190
246
  identity(field: string): FieldIdentity;
247
+ /**
248
+ * The field's current bijection, or null if nothing has asked for it since
249
+ * the last reset. Unlike `identity`, never creates one: for a holder checking
250
+ * whether the identity it applied is still the live one, on every frame.
251
+ */
252
+ peekIdentity(field: string): FieldIdentity | null;
191
253
  /** Fields the view for `table` currently exposes. Empty when there is no view. */
192
254
  columnsFor(table: string): string[];
193
255
  /**
@@ -200,24 +262,34 @@ export declare class CategoricalIdentity {
200
262
  categorical?: (string | IdentityField)[];
201
263
  }): Promise<void>;
202
264
  /**
203
- * Reserve an id for a value that exists in no table — the synthetic "Other"
204
- * bucket a `topN({ includeOthers })` produces.
265
+ * Acquire an id for a value that exists in no table — the synthetic "Other"
266
+ * bucket a `topN({ includeOthers })` produces — and return it with the label
267
+ * it displays.
268
+ *
269
+ * A released id with the same label is reused before a dim row is inserted,
270
+ * so remounting a chart does not grow the dim or the field's activity block.
271
+ * Keyed by label too, so a reused id never needs relabelling.
205
272
  *
206
- * Goes through the same writer as every other id, so `values(f)[otherId]` is
207
- * the label and no consumer has to know it was synthetic. Serialized on the
208
- * per-field chain, so it cannot interleave with an append.
273
+ * Goes through the same writer as every other id, so `values(f)[id]` is the
274
+ * label; `synthetic` records that it is a bucket, which keeps it out of the
275
+ * value -> id map. Serialized on the per-field chain, so it cannot interleave
276
+ * with an append.
209
277
  *
210
278
  * Non-negative by construction: `scaleBand` discards `value < 0.0` before the
211
279
  * slot lookup, so a negative sentinel could never render.
212
280
  *
213
- * **Reuses the id if a real category already carries this label**, rather
214
- * than inserting a duplicate value — see the fan-out note above. Callers get
215
- * an id that may already be in the data, which the transform has to fold
216
- * into rather than append beside.
281
+ * **A real category carrying the label stays a category.** The bucket takes
282
+ * `"<label> (rest)"` instead, so two bars never share a label and the real
283
+ * one stays selectable. The check runs at acquire; a reload re-acquires.
217
284
  *
218
285
  * Requires the dim to exist: call after `ensureColumns` has resolved.
219
286
  */
220
- reserveSynthetic(field: string, label: string): Promise<number>;
287
+ acquireSynthetic(field: string, label: string): Promise<SyntheticId>;
288
+ /**
289
+ * Give an acquired id back for the next acquire of the same label. One from
290
+ * before a reset names a row of a dropped dim, and is ignored.
291
+ */
292
+ releaseSynthetic(synthetic: SyntheticId): void;
221
293
  /**
222
294
  * Drop every view and dim and forget all state. Called on data reload, where
223
295
  * the ids are stale by definition — the same values may not exist any more,