@fjader/svelte 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/components/axes/AxisBottom.svelte +23 -6
- package/dist/components/axes/AxisBottom.svelte.d.ts +10 -0
- package/dist/components/axes/AxisLeft.svelte +77 -8
- package/dist/components/axes/AxisLeft.svelte.d.ts +17 -0
- package/dist/components/axes/BandAxisTicks.svelte +1 -1
- package/dist/components/axes/axis-title.d.ts +30 -0
- package/dist/components/axes/axis-title.js +77 -0
- package/dist/components/charts/ChartCell.svelte +1 -1
- package/dist/components/charts/ChartContainer.svelte +10 -2
- package/dist/components/charts/ChartContainer.svelte.d.ts +7 -0
- package/dist/components/guides/legend/ContinuousColorLegend.svelte +7 -1
- package/dist/components/guides/legend/OrdinalColorLegend.svelte +8 -5
- package/dist/components/marks/AreaMarkX.svelte +2 -0
- package/dist/components/marks/AreaMarkY.svelte +2 -0
- package/dist/components/marks/BarMarkX.svelte +13 -1
- package/dist/components/marks/BarMarkY.svelte +13 -1
- package/dist/components/marks/DotsMark.svelte +2 -0
- package/dist/components/marks/LineMark.svelte +5 -1
- package/dist/components/marks/ParallelCoordinatesMark.svelte +4 -0
- package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +2 -0
- package/dist/components/marks/RectMark.svelte +2 -0
- package/dist/components/marks/RectXMark.svelte +2 -0
- package/dist/components/marks/RectYMark.svelte +2 -0
- package/dist/components/marks/RuleMarkX.svelte +2 -0
- package/dist/components/marks/RuleMarkY.svelte +2 -0
- package/dist/components/marks/TextMark.svelte +8 -9
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +6 -1
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +187 -18
- package/dist/components/marks/lifecycle/domain-queries.d.ts +3 -1
- package/dist/components/marks/lifecycle/domain-queries.js +11 -6
- package/dist/components/marks/lifecycle/domain-registration.js +2 -6
- package/dist/components/marks/lifecycle/other-rows.svelte.d.ts +104 -0
- package/dist/components/marks/lifecycle/other-rows.svelte.js +193 -0
- package/dist/components/marks/lifecycle/other-split.d.ts +80 -0
- package/dist/components/marks/lifecycle/other-split.js +112 -0
- package/dist/components/marks/lifecycle/query-builder.d.ts +26 -1
- package/dist/components/marks/lifecycle/query-builder.js +25 -5
- package/dist/components/marks/utils/reactive-config.d.ts +1 -1
- package/dist/components/marks/utils/reactive-config.js +6 -0
- package/dist/components/marks/utils/tooltip-helpers.d.ts +4 -1
- package/dist/components/marks/utils/tooltip-helpers.js +2 -1
- package/dist/components/rendering/BaseGPURenderer.svelte.d.ts +3 -1
- package/dist/components/rendering/BaseGPURenderer.svelte.js +4 -2
- package/dist/components/rendering/Rects.svelte +117 -23
- package/dist/components/rendering/Rects.svelte.d.ts +7 -0
- package/dist/components/rendering/activity-reads.d.ts +18 -0
- package/dist/components/rendering/activity-reads.js +19 -0
- package/dist/config/ChartConfig.svelte.d.ts +3 -1
- package/dist/gpu/MorphedChannels.d.ts +6 -32
- package/dist/gpu/MorphedChannels.js +45 -3
- package/dist/gpu/ScaleTemplateSystem.js +25 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/interactors/Interval1D.svelte +6 -1
- package/dist/interactors/Interval2D.svelte +6 -1
- package/dist/interactors/ParallelAxisSource.svelte +10 -1
- package/dist/interactors/Toggle.svelte +32 -3
- package/dist/interactors/Tooltip.svelte +138 -41
- package/dist/interactors/TooltipManager.svelte.d.ts +20 -5
- package/dist/interactors/TooltipManager.svelte.js +32 -7
- package/dist/interactors/other-tooltip.d.ts +16 -0
- package/dist/interactors/other-tooltip.js +22 -0
- package/dist/query-engine/FieldActivity.svelte.d.ts +50 -1
- package/dist/query-engine/FieldActivity.svelte.js +176 -5
- package/dist/query-engine/FilterGroup.svelte.d.ts +23 -1
- package/dist/query-engine/FilterGroup.svelte.js +43 -3
- package/dist/query-engine/FilterGroupRegistry.svelte.d.ts +31 -3
- package/dist/query-engine/FilterGroupRegistry.svelte.js +62 -15
- package/dist/query-engine/QueryEngine.js +26 -6
- package/dist/query-engine/categorical-identity.d.ts +15 -0
- package/dist/query-engine/categorical-identity.js +15 -0
- package/dist/query-engine/preagg/plan.d.ts +5 -0
- package/dist/query-engine/preagg/plan.js +31 -7
- package/dist/query-engine/toggle-predicate.d.ts +21 -0
- package/dist/query-engine/toggle-predicate.js +29 -1
- package/dist/scales/ActivityTable.d.ts +5 -1
- package/dist/scales/ActivityTable.js +12 -3
- package/dist/scales/BandScaleState.svelte.d.ts +13 -8
- package/dist/scales/BandScaleState.svelte.js +23 -16
- package/dist/spec/types.d.ts +2 -0
- package/dist/theme.css +5 -4
- package/dist/transforms/TransformCompiler.d.ts +37 -2
- package/dist/transforms/TransformCompiler.js +44 -1
- package/dist/transforms/index.js +32 -18
- package/dist/types.d.ts +41 -0
- package/package.json +3 -2
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
table,
|
|
49
49
|
selection = undefined,
|
|
50
50
|
selectionResponse = undefined,
|
|
51
|
+
nulls = undefined,
|
|
51
52
|
x = null,
|
|
52
53
|
x1: x1Prop = null,
|
|
53
54
|
x2: x2Prop = null,
|
|
@@ -97,6 +98,7 @@
|
|
|
97
98
|
transforms: () => transforms,
|
|
98
99
|
selection: () => selection,
|
|
99
100
|
selectionResponse: () => selectionResponse,
|
|
101
|
+
nulls: () => nulls,
|
|
100
102
|
limit: () => limit,
|
|
101
103
|
preferredXScaleType: () => preferredXScaleType,
|
|
102
104
|
preferredYScaleType: () => preferredYScaleType,
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
table,
|
|
49
49
|
selection = undefined,
|
|
50
50
|
selectionResponse = undefined,
|
|
51
|
+
nulls = undefined,
|
|
51
52
|
x,
|
|
52
53
|
x1: x1Prop,
|
|
53
54
|
x2: x2Prop,
|
|
@@ -92,6 +93,7 @@
|
|
|
92
93
|
transforms: () => transforms,
|
|
93
94
|
selection: () => selection,
|
|
94
95
|
selectionResponse: () => selectionResponse,
|
|
96
|
+
nulls: () => nulls,
|
|
95
97
|
limit: () => limit,
|
|
96
98
|
preferredYScaleType: () => preferredYScaleType,
|
|
97
99
|
fitDomain: () => fitDomain,
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
table,
|
|
50
50
|
selection = undefined,
|
|
51
51
|
selectionResponse = undefined,
|
|
52
|
+
nulls = undefined,
|
|
52
53
|
x = null,
|
|
53
54
|
x1: x1Prop = null,
|
|
54
55
|
x2: x2Prop = null,
|
|
@@ -94,6 +95,7 @@
|
|
|
94
95
|
transforms: () => transforms,
|
|
95
96
|
selection: () => selection,
|
|
96
97
|
selectionResponse: () => selectionResponse,
|
|
98
|
+
nulls: () => nulls,
|
|
97
99
|
limit: () => limit,
|
|
98
100
|
preferredXScaleType: () => preferredXScaleType,
|
|
99
101
|
fitDomain: () => fitDomain,
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
table = undefined,
|
|
37
37
|
selection = undefined,
|
|
38
38
|
selectionResponse = undefined,
|
|
39
|
+
nulls = undefined,
|
|
39
40
|
x,
|
|
40
41
|
y: yShorthand = undefined,
|
|
41
42
|
y1: y1Prop = undefined,
|
|
@@ -124,6 +125,7 @@
|
|
|
124
125
|
transforms: () => transforms,
|
|
125
126
|
selection: () => selection,
|
|
126
127
|
selectionResponse: () => selectionResponse,
|
|
128
|
+
nulls: () => nulls,
|
|
127
129
|
limit: () => limit,
|
|
128
130
|
sort: () => sort,
|
|
129
131
|
domain: () => domain,
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
table = undefined,
|
|
37
37
|
selection = undefined,
|
|
38
38
|
selectionResponse = undefined,
|
|
39
|
+
nulls = undefined,
|
|
39
40
|
y,
|
|
40
41
|
x: xShorthand = undefined,
|
|
41
42
|
x1: x1Prop = undefined,
|
|
@@ -122,6 +123,7 @@
|
|
|
122
123
|
transforms: () => transforms,
|
|
123
124
|
selection: () => selection,
|
|
124
125
|
selectionResponse: () => selectionResponse,
|
|
126
|
+
nulls: () => nulls,
|
|
125
127
|
limit: () => limit,
|
|
126
128
|
sort: () => sort,
|
|
127
129
|
domain: () => domain,
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
table,
|
|
44
44
|
selection = undefined,
|
|
45
45
|
selectionResponse = undefined,
|
|
46
|
+
nulls = undefined,
|
|
46
47
|
x,
|
|
47
48
|
y,
|
|
48
49
|
text,
|
|
@@ -131,6 +132,9 @@
|
|
|
131
132
|
get selectionResponse() {
|
|
132
133
|
return selectionResponse;
|
|
133
134
|
},
|
|
135
|
+
get nulls() {
|
|
136
|
+
return nulls;
|
|
137
|
+
},
|
|
134
138
|
get limit() {
|
|
135
139
|
return limit;
|
|
136
140
|
},
|
|
@@ -387,17 +391,12 @@
|
|
|
387
391
|
// range: a label off the left edge, or below the axis, where the shader
|
|
388
392
|
// discards the bar it belongs to. The GPU states this discard explicitly
|
|
389
393
|
// rather than leaving it to the scissor rect, and so does this.
|
|
390
|
-
const slotted = (
|
|
391
|
-
band: BandScaleState,
|
|
392
|
-
raw: string | number,
|
|
393
|
-
range: number,
|
|
394
|
-
axis: 'x' | 'y'
|
|
395
|
-
) => {
|
|
394
|
+
const slotted = (band: BandScaleState, raw: string | number, range: number) => {
|
|
396
395
|
const key = bandSlotKey(band, raw);
|
|
397
396
|
if (key == null) return null;
|
|
398
397
|
// Asked once and handed on: tickPosition would look it up again.
|
|
399
398
|
const slot = band.tickSlot(key);
|
|
400
|
-
return slot < 0 ? null : band.slotPosition(slot, range
|
|
399
|
+
return slot < 0 ? null : band.slotPosition(slot, range);
|
|
401
400
|
};
|
|
402
401
|
|
|
403
402
|
const newPos = new Array(items.length);
|
|
@@ -406,13 +405,13 @@
|
|
|
406
405
|
let px, py;
|
|
407
406
|
|
|
408
407
|
if (xBand) {
|
|
409
|
-
px = slotted(xBand, xRaw[dataIdx], rangeX
|
|
408
|
+
px = slotted(xBand, xRaw[dataIdx], rangeX);
|
|
410
409
|
} else {
|
|
411
410
|
px = xScale(xRaw[dataIdx]);
|
|
412
411
|
}
|
|
413
412
|
|
|
414
413
|
if (yBand) {
|
|
415
|
-
py = slotted(yBand, yRaw[dataIdx], rangeY
|
|
414
|
+
py = slotted(yBand, yRaw[dataIdx], rangeY);
|
|
416
415
|
} else {
|
|
417
416
|
py = yScale(yRaw[dataIdx]);
|
|
418
417
|
}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import type { ScaleRequirement } from '../../../scales/ScalePriorities';
|
|
21
21
|
import type { DataAccessor } from '../../../data/DataAccessor';
|
|
22
|
-
import type { FieldRef, NormalizedSortConfig, Transform, ChartContext } from '../../../types';
|
|
22
|
+
import type { FieldRef, NormalizedSortConfig, Transform, ChartContext, NullsResponse } from '../../../types';
|
|
23
23
|
import type { ColorScaleState } from '../../../scales/ColorScaleState.svelte';
|
|
24
24
|
import type { SizeScaleState } from '../../../scales/SizeScaleState.svelte';
|
|
25
25
|
import type { QueryEngine } from '../../../query-engine/QueryEngine';
|
|
@@ -27,6 +27,7 @@ import type { FilterGroup } from '../../../query-engine/FilterGroup.svelte';
|
|
|
27
27
|
import { type SelectionResponse } from '../../rendering/activity-reads';
|
|
28
28
|
import type { MarkLODSurface } from '../../../lod/rung-types';
|
|
29
29
|
import type { GPUCoordinator } from '../../../gpu/GPUCoordinator.svelte';
|
|
30
|
+
import { OtherRows } from './other-rows.svelte';
|
|
30
31
|
export interface MarkLifecycleV2Config {
|
|
31
32
|
markType: string;
|
|
32
33
|
supportedScaleTypes: {
|
|
@@ -93,6 +94,8 @@ export interface ReactiveConfig {
|
|
|
93
94
|
readonly fitDomain: 'x' | 'y' | boolean | undefined;
|
|
94
95
|
readonly tip: (unknown | '*')[] | false | undefined;
|
|
95
96
|
readonly selectionResponse: SelectionResponse | undefined;
|
|
97
|
+
readonly nulls: NullsResponse | undefined;
|
|
98
|
+
readonly contributeDomain: boolean | undefined;
|
|
96
99
|
}
|
|
97
100
|
export declare class MarkLifecycleV2 {
|
|
98
101
|
#private;
|
|
@@ -101,6 +104,8 @@ export declare class MarkLifecycleV2 {
|
|
|
101
104
|
gpuCoordinator?: GPUCoordinator | null;
|
|
102
105
|
onMarkLOD?: ((surface: MarkLODSurface | null) => void) | null;
|
|
103
106
|
});
|
|
107
|
+
/** What this mark's Other rows hold, and what is done with it. */
|
|
108
|
+
readonly otherRows: OtherRows;
|
|
104
109
|
/**
|
|
105
110
|
* The axis whose rows are one per category, in category order — or null.
|
|
106
111
|
*
|
|
@@ -42,6 +42,7 @@ import { buildQueryParts, assembleQuery, castReal } from './query-builder';
|
|
|
42
42
|
import { setScalesReady, markDeferredScalesReady, setInitialContinuousDomains, registerDataDerivedDomains } from './domain-registration';
|
|
43
43
|
import { fetchCategoricalDomains, fetchGlobalAggregates } from './domain-queries';
|
|
44
44
|
import { transformScaleKey } from './transform-scale-key';
|
|
45
|
+
import { OtherRows } from './other-rows.svelte';
|
|
45
46
|
// ---------------------------------------------------------------------------
|
|
46
47
|
// MarkLifecycleV2
|
|
47
48
|
// ---------------------------------------------------------------------------
|
|
@@ -185,8 +186,68 @@ export class MarkLifecycleV2 {
|
|
|
185
186
|
this.#inlineIdentityColumns(resolvedSelect);
|
|
186
187
|
this.#inlineTimeOffsets(resolvedSelect);
|
|
187
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;
|
|
188
192
|
return parts;
|
|
189
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
|
+
}
|
|
190
251
|
// --- Domain State ---
|
|
191
252
|
#channelsNeedingDataDomain = new Set();
|
|
192
253
|
#dataDerivedDomainsSet = $state(false);
|
|
@@ -257,6 +318,25 @@ export class MarkLifecycleV2 {
|
|
|
257
318
|
// than rebuilding. With no sort it passes the query through.
|
|
258
319
|
this.#transformCompiler = new TransformCompiler(withRanking(withDenseCategories(config.transforms || [], config.dense !== false), () => this.#rankingSort), this.#transformConfig);
|
|
259
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
|
+
});
|
|
260
340
|
// AdaptiveBinRung for binned marks (watches viewport domain, triggers rebin)
|
|
261
341
|
if (this.#autoBinNodes.size > 0 && options?.gpuCoordinator) {
|
|
262
342
|
const binAxes = [...this.#autoBinNodes.keys()].map((ch) => (ch.startsWith('x') ? 'x' : 'y'));
|
|
@@ -342,7 +422,8 @@ export class MarkLifecycleV2 {
|
|
|
342
422
|
return;
|
|
343
423
|
this.#identityFields.clear();
|
|
344
424
|
// The Other ids name rows of the dropped dims. Every holder lets go, so
|
|
345
|
-
// the scale acquires afresh when setup re-runs.
|
|
425
|
+
// the scale acquires afresh when setup re-runs; so do the registered rows.
|
|
426
|
+
this.otherRows.unregister();
|
|
346
427
|
for (const scale of [...this.#otherScales])
|
|
347
428
|
this.#releaseOther(scale);
|
|
348
429
|
// Every dim it names just went away — topN must not bucket into one.
|
|
@@ -491,14 +572,35 @@ export class MarkLifecycleV2 {
|
|
|
491
572
|
return;
|
|
492
573
|
untrack(() => {
|
|
493
574
|
this.#handleDataDomains(dv);
|
|
575
|
+
this.otherRows.register();
|
|
494
576
|
});
|
|
495
577
|
});
|
|
496
|
-
// Tooltip:
|
|
578
|
+
// Tooltip: two effects, so a data update does not take the table away.
|
|
579
|
+
//
|
|
580
|
+
// This one owns the table's lifetime: it drops it when tooltips go from
|
|
581
|
+
// wanted to not. It used to be one effect with the write below, and its
|
|
582
|
+
// cleanup then ran on every data update too: the table was dropped and
|
|
583
|
+
// marked not ready, the mark's tooltip unregistered, and the hovered row
|
|
584
|
+
// went with it. A click right after a data update found no row under the
|
|
585
|
+
// pointer, read as a click on empty space and cleared the selection, and
|
|
586
|
+
// the next click did nothing until the pointer moved.
|
|
587
|
+
$effect(() => {
|
|
588
|
+
const tooltipConfig = this.#reactiveConfig.tooltip;
|
|
589
|
+
const fitDomain = this.#reactiveConfig.fitDomain;
|
|
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.
|
|
497
600
|
$effect(() => {
|
|
498
601
|
const tooltipConfig = this.#reactiveConfig.tooltip;
|
|
499
602
|
const fitDomain = this.#reactiveConfig.fitDomain;
|
|
500
|
-
|
|
501
|
-
if (!tooltipWanted)
|
|
603
|
+
if (tooltipConfig === false && !fitDomain)
|
|
502
604
|
return;
|
|
503
605
|
const dataVersion = this.dataVersion;
|
|
504
606
|
if (dataVersion === 0)
|
|
@@ -511,12 +613,7 @@ export class MarkLifecycleV2 {
|
|
|
511
613
|
this.#writeTooltipTable(data);
|
|
512
614
|
}, 300);
|
|
513
615
|
});
|
|
514
|
-
|
|
515
|
-
return () => {
|
|
516
|
-
clearTimeout(this.#tooltipWriteTimeout);
|
|
517
|
-
this.#tooltipTableReady = false;
|
|
518
|
-
this.#queryEngine.exec(`DROP TABLE IF EXISTS "${this.#tooltipTableName}"`).catch(() => { });
|
|
519
|
-
};
|
|
616
|
+
return () => clearTimeout(this.#tooltipWriteTimeout);
|
|
520
617
|
});
|
|
521
618
|
// fitDomain — query DuckDB for fitted axis extent on driving axis change.
|
|
522
619
|
$effect(() => {
|
|
@@ -612,7 +709,7 @@ export class MarkLifecycleV2 {
|
|
|
612
709
|
const [schemaColumns, fetchCount, aggResult, fieldStats] = await Promise.all([
|
|
613
710
|
// Fetch table schema (cached on QueryEngine, needed for CAST type decisions)
|
|
614
711
|
this.#queryEngine.describe(this.#config.table),
|
|
615
|
-
fetchCategoricalDomains(this.#queryEngine, this.#config.table, this.#config.select, this.actualXScaleType, this.actualYScaleType, this.#
|
|
712
|
+
fetchCategoricalDomains(this.#queryEngine, this.#config.table, this.#config.select, this.actualXScaleType, this.actualYScaleType, this.#bandScaleForDomain('x'), this.#bandScaleForDomain('y'), this.#chartContext.facetBandScale, this.#chartContext.fx, this.#config.colorChannel || 'fill', this.#colorScaleState, this.#config.sizeChannel || 'r', this.#sizeScaleState, this.#seriesScaleState, seriesKey, this.#bandDomainFromData, this.#markId, this.#reactiveConfig.domain, this.#reactiveConfig.nulls === 'drop'),
|
|
616
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),
|
|
617
714
|
this.#fetchFieldStats(),
|
|
618
715
|
this.#ensureIdentityColumns()
|
|
@@ -872,7 +969,7 @@ export class MarkLifecycleV2 {
|
|
|
872
969
|
const additionalChannels = this.#transformCompiler.getAdditionalChannels();
|
|
873
970
|
const xState = this.#chartContext.xScaleState;
|
|
874
971
|
const yState = this.#chartContext.yScaleState;
|
|
875
|
-
const domainResult = registerDataDerivedDomains(data, this.#channelsNeedingDataDomain, additionalChannels, this.#skipDomainUpdateForAxis, this.#config.zeroBaseline, this.actualXScaleType, this.actualYScaleType, xState, yState, this.#bandDomainFromData, this.#
|
|
972
|
+
const domainResult = registerDataDerivedDomains(data, this.#channelsNeedingDataDomain, additionalChannels, this.#skipDomainUpdateForAxis, this.#config.zeroBaseline, this.actualXScaleType, this.actualYScaleType, xState, yState, this.#bandDomainFromData, this.#bandScaleForDomain('x'), this.#bandScaleForDomain('y'), this.#markId, false, // isRebinning
|
|
876
973
|
this.#colorScaleState);
|
|
877
974
|
this.#dataDerivedDomainsSet = domainResult.dataDerivedDomainsSet;
|
|
878
975
|
this.#skipDomainUpdateForAxis = domainResult.skipDomainUpdateForAxis;
|
|
@@ -1007,11 +1104,15 @@ export class MarkLifecycleV2 {
|
|
|
1007
1104
|
const identity = {};
|
|
1008
1105
|
const describe = (channel, key, positional) => {
|
|
1009
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;
|
|
1010
1110
|
identity[channel] = {
|
|
1011
1111
|
dim: this.#queryEngine.dimTableFor(key),
|
|
1012
1112
|
size: this.#queryEngine.identityFor(key).values.length,
|
|
1013
1113
|
positional,
|
|
1014
|
-
...(present ? { present } : {})
|
|
1114
|
+
...(present ? { present } : {}),
|
|
1115
|
+
...(nullId !== null ? { dropNullId: nullId } : {})
|
|
1015
1116
|
};
|
|
1016
1117
|
};
|
|
1017
1118
|
for (const { axis, key } of axes)
|
|
@@ -1136,6 +1237,67 @@ export class MarkLifecycleV2 {
|
|
|
1136
1237
|
this.#releaseOther(scale);
|
|
1137
1238
|
}
|
|
1138
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
|
+
}
|
|
1139
1301
|
/** The band scales whose Other bucket this mark holds a claim on. */
|
|
1140
1302
|
#otherScales = new Set();
|
|
1141
1303
|
#releaseOther(scale) {
|
|
@@ -1195,10 +1357,12 @@ export class MarkLifecycleV2 {
|
|
|
1195
1357
|
// Build raw query parts directly from config.select (no identity columns,
|
|
1196
1358
|
// no bin resolution — scale states aren't ready yet during Phase 1).
|
|
1197
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.
|
|
1198
1362
|
const q = assembleQuery({
|
|
1199
1363
|
table: this.#config.table,
|
|
1200
1364
|
parts,
|
|
1201
|
-
filter,
|
|
1365
|
+
filter: [...filter, ...this.#rawNullFilters(this.#config.select)],
|
|
1202
1366
|
transformCompiler: this.#transformCompiler
|
|
1203
1367
|
});
|
|
1204
1368
|
if (!q)
|
|
@@ -1496,10 +1660,8 @@ export class MarkLifecycleV2 {
|
|
|
1496
1660
|
// the rest are scalars (the rank, id columns), cheap to keep, and the
|
|
1497
1661
|
// lists are the only ones whose size grows with the field.
|
|
1498
1662
|
const withoutMembers = withoutOtherMembers(tooltipQuery);
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
`CREATE TEMP TABLE "${tableName}" AS SELECT *${replaceClause} FROM (${withoutMembers})${orderClause}`
|
|
1502
|
-
]);
|
|
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}`);
|
|
1503
1665
|
}
|
|
1504
1666
|
else {
|
|
1505
1667
|
// Path B: Serialize transformed data via SQL VALUES.
|
|
@@ -1554,6 +1716,9 @@ export class MarkLifecycleV2 {
|
|
|
1554
1716
|
}
|
|
1555
1717
|
}
|
|
1556
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();
|
|
1557
1722
|
this.#onTooltipTableWritten();
|
|
1558
1723
|
}
|
|
1559
1724
|
catch (error) {
|
|
@@ -1624,6 +1789,9 @@ export class MarkLifecycleV2 {
|
|
|
1624
1789
|
}
|
|
1625
1790
|
Object.assign(sidecarSelect, tipExtras);
|
|
1626
1791
|
const sidecarParts = buildQueryParts(sidecarSelect, undefined, this.#transformCompiler.hasSQLTransforms());
|
|
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;
|
|
1627
1795
|
const filter = this.#filterGroup.selection.predicate(this.#route) ?? [];
|
|
1628
1796
|
const sidecarQuery = assembleQuery({
|
|
1629
1797
|
table: this.#sourceTable,
|
|
@@ -1822,6 +1990,7 @@ export class MarkLifecycleV2 {
|
|
|
1822
1990
|
this.#destroyed = true;
|
|
1823
1991
|
this.#identityResetOff?.();
|
|
1824
1992
|
this.#identityResetOff = null;
|
|
1993
|
+
this.otherRows.unregister();
|
|
1825
1994
|
for (const scale of [...this.#otherScales])
|
|
1826
1995
|
this.#releaseOther(scale);
|
|
1827
1996
|
this.#handle?.destroy();
|
|
@@ -14,7 +14,9 @@ import type { ContinuousScaleState } from '../../../types';
|
|
|
14
14
|
export declare function fetchCategoricalDomains(coordinator: QueryEngine, table: string, select: Record<string, unknown>, actualXScaleType: string, actualYScaleType: string, xScaleState: BandScaleState | null, yScaleState: BandScaleState | null, facetBandScale: BandScaleState | null | undefined, fx: unknown, colorChannel: string, colorScaleState: ColorScaleState | null, sizeChannel: string, sizeScaleState: SizeScaleState | null, seriesScaleState: ColorScaleState | null, seriesChannel: string | null, bandDomainFromData: boolean, markId: string, explicitDomain?: {
|
|
15
15
|
x?: DimValue[];
|
|
16
16
|
y?: DimValue[];
|
|
17
|
-
} | null
|
|
17
|
+
} | null,
|
|
18
|
+
/** nulls="drop": the band and facet domains leave out the NULL category. */
|
|
19
|
+
dropNull?: boolean): Promise<number>;
|
|
18
20
|
/**
|
|
19
21
|
* Fetch global min/max extents for aggregated fields using a "sidecar" query.
|
|
20
22
|
* Runs the same aggregation as the main query but without filters, wrapped
|
|
@@ -10,7 +10,13 @@ import { identityKeyFor } from '../../../query-engine/identity-key';
|
|
|
10
10
|
*
|
|
11
11
|
* Returns the number of fetches executed (for logging).
|
|
12
12
|
*/
|
|
13
|
-
export async function fetchCategoricalDomains(coordinator, table, select, actualXScaleType, actualYScaleType, xScaleState, yScaleState, facetBandScale, fx, colorChannel, colorScaleState, sizeChannel, sizeScaleState, seriesScaleState, seriesChannel, bandDomainFromData, markId, explicitDomain
|
|
13
|
+
export async function fetchCategoricalDomains(coordinator, table, select, actualXScaleType, actualYScaleType, xScaleState, yScaleState, facetBandScale, fx, colorChannel, colorScaleState, sizeChannel, sizeScaleState, seriesScaleState, seriesChannel, bandDomainFromData, markId, explicitDomain,
|
|
14
|
+
/** nulls="drop": the band and facet domains leave out the NULL category. */
|
|
15
|
+
dropNull = false) {
|
|
16
|
+
const categoriesOf = (key) => {
|
|
17
|
+
const categories = coordinator.categoryValues(key, table);
|
|
18
|
+
return dropNull ? categories.filter((c) => c !== null) : categories;
|
|
19
|
+
};
|
|
14
20
|
const fetches = [];
|
|
15
21
|
const setupFetch = (axis, field, scaleState) => {
|
|
16
22
|
if (!field || !scaleState)
|
|
@@ -65,7 +71,7 @@ export async function fetchCategoricalDomains(coordinator, table, select, actual
|
|
|
65
71
|
// Idempotent and coalesced — the mark's own setup calls this too.
|
|
66
72
|
await coordinator.ensureColumns(table, { categorical: [identityField] });
|
|
67
73
|
// This table's categories: the dim spans every table on the field.
|
|
68
|
-
const categories =
|
|
74
|
+
const categories = categoriesOf(identityField.key);
|
|
69
75
|
// A snapshot of the real categories. The scale adopts the live
|
|
70
76
|
// identity itself in MarkLifecycleV2's #ensureIdentityColumns, which
|
|
71
77
|
// also covers the explicit-domain axes that never reach this branch.
|
|
@@ -103,9 +109,8 @@ export async function fetchCategoricalDomains(coordinator, table, select, actual
|
|
|
103
109
|
// Registered from the data.
|
|
104
110
|
}
|
|
105
111
|
else if (explicitDomain?.y && yScaleState) {
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
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;
|
|
109
114
|
}
|
|
110
115
|
else {
|
|
111
116
|
setupFetch('y', select.y, yScaleState);
|
|
@@ -123,7 +128,7 @@ export async function fetchCategoricalDomains(coordinator, table, select, actual
|
|
|
123
128
|
fetches.push((async () => {
|
|
124
129
|
// Idempotent and coalesced — the mark's own setup calls this too.
|
|
125
130
|
await coordinator.ensureColumns(table, { categorical: [facetKey] });
|
|
126
|
-
const categories =
|
|
131
|
+
const categories = categoriesOf(facetKey.key);
|
|
127
132
|
if (categories.length > 0)
|
|
128
133
|
facetBandScale.addMark(categories, markId);
|
|
129
134
|
})());
|
|
@@ -193,12 +193,8 @@ export function registerDataDerivedDomains(data, channelsNeedingDataDomain, addi
|
|
|
193
193
|
// order the ranked query had. See RANK_COLUMN.
|
|
194
194
|
const ranked = data.getColumn(RANK_COLUMN) != null;
|
|
195
195
|
const ids = extractIdsRankedFromData(data, axis, RANK_COLUMN);
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
// topN's and the sort's alike — rather than by whichever transform
|
|
199
|
-
// produced the rank.
|
|
200
|
-
if (ranked && axis === 'y')
|
|
201
|
-
ids.reverse();
|
|
196
|
+
// Used as ranked on both axes: slot 0 is at the left of x and the
|
|
197
|
+
// top of y, where a ranking is read from.
|
|
202
198
|
if (ids.length === 0)
|
|
203
199
|
continue;
|
|
204
200
|
// `sorted` says this list is a ranking the axis should prefer over
|