@fjader/svelte 0.2.4 → 0.2.5
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 +1 -1
- package/dist/animation/EntranceGate.d.ts +52 -0
- package/dist/animation/EntranceGate.js +93 -0
- package/dist/animation/TransitionClock.d.ts +61 -30
- package/dist/animation/TransitionClock.js +67 -29
- package/dist/animation/transition-log.d.ts +41 -0
- package/dist/animation/transition-log.js +57 -0
- package/dist/components/axes/AxisBottom.svelte +4 -2
- package/dist/components/axes/AxisCenter.svelte +4 -2
- package/dist/components/axes/AxisLeft.svelte +4 -2
- package/dist/components/axes/BandAxisTicks.svelte +3 -2
- package/dist/components/charts/ChartCell.svelte +32 -5
- package/dist/components/charts/ChartCell.svelte.d.ts +9 -0
- package/dist/components/charts/ChartContainer.svelte +45 -3
- package/dist/components/charts/ChartContainer.svelte.d.ts +7 -1
- package/dist/components/charts/ChartHover.svelte.d.ts +14 -0
- package/dist/components/charts/ChartHover.svelte.js +22 -0
- package/dist/components/charts/ChartVisibility.svelte.d.ts +43 -0
- package/dist/components/charts/ChartVisibility.svelte.js +67 -0
- package/dist/components/guides/Grid.svelte +12 -1
- package/dist/components/marks/AreaMarkX.svelte +16 -4
- package/dist/components/marks/AreaMarkY.svelte +16 -4
- package/dist/components/marks/BarMarkX.svelte +7 -3
- package/dist/components/marks/BarMarkY.svelte +7 -3
- package/dist/components/marks/DotsMark.svelte +6 -3
- package/dist/components/marks/LineMark.svelte +11 -8
- package/dist/components/marks/ParallelCoordinatesMark.svelte +7 -2
- package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +2 -0
- package/dist/components/marks/RectMark.svelte +6 -3
- package/dist/components/marks/RectXMark.svelte +7 -3
- package/dist/components/marks/RectYMark.svelte +7 -3
- package/dist/components/marks/RuleMarkX.svelte +30 -26
- package/dist/components/marks/RuleMarkY.svelte +30 -26
- package/dist/components/marks/TextMark.svelte +44 -5
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +16 -0
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +398 -54
- package/dist/components/marks/lifecycle/domain-queries.d.ts +11 -2
- package/dist/components/marks/lifecycle/domain-queries.js +33 -6
- package/dist/components/marks/lifecycle/domain-registration.d.ts +6 -1
- package/dist/components/marks/lifecycle/domain-registration.js +15 -14
- package/dist/components/marks/lifecycle/other-rows.svelte.d.ts +9 -1
- package/dist/components/marks/lifecycle/other-rows.svelte.js +2 -2
- package/dist/components/marks/lifecycle/query-builder.d.ts +27 -2
- package/dist/components/marks/lifecycle/query-builder.js +23 -20
- package/dist/components/marks/utils/MarkColor.svelte.d.ts +3 -1
- package/dist/components/marks/utils/MarkColor.svelte.js +33 -3
- package/dist/components/marks/utils/MarkSeries.svelte.d.ts +14 -1
- package/dist/components/marks/utils/MarkSeries.svelte.js +25 -1
- package/dist/components/marks/utils/MarkSize.svelte.js +4 -0
- package/dist/components/marks/utils/reactive-config.d.ts +1 -1
- package/dist/components/marks/utils/reactive-config.js +3 -0
- package/dist/components/rendering/Areas.svelte +57 -19
- package/dist/components/rendering/Areas.svelte.d.ts +12 -0
- package/dist/components/rendering/BaseGPURenderer.svelte.d.ts +1 -1
- package/dist/components/rendering/Dots.svelte +23 -3
- package/dist/components/rendering/Dots.svelte.d.ts +6 -0
- package/dist/components/rendering/GridLine.svelte +13 -1
- package/dist/components/rendering/GridLine.svelte.d.ts +5 -0
- package/dist/components/rendering/Lines.svelte +48 -13
- package/dist/components/rendering/Lines.svelte.d.ts +12 -0
- package/dist/components/rendering/ParallelLines.svelte +5 -5
- package/dist/components/rendering/Rects.svelte +51 -11
- package/dist/components/rendering/Rects.svelte.d.ts +16 -2
- package/dist/components/rendering/RuleX.svelte +1 -1
- package/dist/components/rendering/RuleY.svelte +1 -1
- package/dist/components/rendering/WebGPUCanvas.svelte +4 -2
- package/dist/components/rendering/activity-reads.d.ts +41 -24
- package/dist/components/rendering/activity-reads.js +54 -31
- package/dist/components/rendering/uniform-sizes.d.ts +1 -1
- package/dist/components/rendering/uniform-sizes.js +1 -1
- package/dist/config/ChartConfig.svelte.d.ts +7 -0
- package/dist/config/ChartConfig.svelte.js +17 -0
- package/dist/gpu/GPUCoordinator.svelte.d.ts +31 -1
- package/dist/gpu/GPUCoordinator.svelte.js +164 -7
- package/dist/gpu/MorphedChannels.d.ts +11 -2
- package/dist/gpu/MorphedChannels.js +33 -11
- package/dist/gpu/ScaleTemplateSystem.d.ts +20 -2
- package/dist/gpu/ScaleTemplateSystem.js +58 -2
- package/dist/index.d.ts +17 -4
- package/dist/index.js +19 -4
- package/dist/interactors/TooltipManager.svelte.js +14 -5
- package/dist/query-engine/CrossfilterSelection.svelte.d.ts +4 -2
- package/dist/query-engine/CrossfilterSelection.svelte.js +2 -0
- package/dist/query-engine/FieldActivity.svelte.d.ts +2 -3
- package/dist/query-engine/FieldActivity.svelte.js +1 -2
- package/dist/query-engine/MarkHandle.svelte.d.ts +28 -1
- package/dist/query-engine/MarkHandle.svelte.js +33 -1
- package/dist/query-engine/QueryEngine.d.ts +225 -5
- package/dist/query-engine/QueryEngine.js +671 -41
- package/dist/query-engine/categorical-identity.d.ts +34 -8
- package/dist/query-engine/categorical-identity.js +138 -73
- package/dist/query-engine/debug/query-log.d.ts +39 -0
- package/dist/query-engine/debug/query-log.js +90 -0
- package/dist/query-engine/duckdb.d.ts +55 -8
- package/dist/query-engine/duckdb.js +82 -30
- package/dist/query-engine/index.d.ts +7 -4
- package/dist/query-engine/index.js +3 -3
- package/dist/query-engine/preagg/BuildQueue.d.ts +33 -13
- package/dist/query-engine/preagg/BuildQueue.js +51 -15
- package/dist/query-engine/preagg/Preagg.d.ts +46 -3
- package/dist/query-engine/preagg/Preagg.js +159 -31
- package/dist/query-engine/preagg/plan.d.ts +28 -1
- package/dist/query-engine/preagg/plan.js +128 -8
- package/dist/query-engine/sliced-lane.d.ts +108 -0
- package/dist/query-engine/sliced-lane.js +299 -0
- package/dist/query-engine/source-routing.d.ts +106 -0
- package/dist/query-engine/source-routing.js +189 -0
- package/dist/scales/BandScaleState.svelte.d.ts +9 -1
- package/dist/scales/BandScaleState.svelte.js +16 -5
- package/dist/scales/ChartScales.svelte.d.ts +3 -0
- package/dist/scales/ChartScales.svelte.js +26 -8
- package/dist/scales/ColorScaleState.svelte.d.ts +18 -2
- package/dist/scales/ColorScaleState.svelte.js +138 -26
- package/dist/scales/LinearScaleState.svelte.d.ts +7 -10
- package/dist/scales/LinearScaleState.svelte.js +21 -39
- package/dist/scales/ScaleGeneration.svelte.d.ts +48 -0
- package/dist/scales/ScaleGeneration.svelte.js +42 -0
- package/dist/scales/ScaleRegistry.svelte.js +3 -1
- package/dist/scales/SlotTransition.d.ts +15 -1
- package/dist/scales/SlotTransition.js +33 -4
- package/dist/scales/TimeScaleState.svelte.d.ts +7 -3
- package/dist/scales/TimeScaleState.svelte.js +7 -7
- package/dist/scales/color-utils.d.ts +0 -3
- package/dist/scales/color-utils.js +0 -3
- package/dist/scales/entered.d.ts +8 -0
- package/dist/scales/entered.js +10 -0
- package/dist/spec/LazyQueryEngine.js +2 -19
- package/dist/spec/MarkRegistry.d.ts +3 -1
- package/dist/spec/MarkRegistry.js +13 -1
- package/dist/spec/PlotFromSpec.svelte +1 -0
- package/dist/spec/export.js +11 -2
- package/dist/spec/types.d.ts +41 -13
- package/dist/sql/bin.d.ts +25 -0
- package/dist/sql/bin.js +108 -5
- package/dist/transforms/TransformCompiler.d.ts +10 -0
- package/dist/transforms/TransformCompiler.js +26 -0
- package/dist/transforms/index.js +144 -24
- package/dist/types.d.ts +136 -2
- package/package.json +2 -2
|
@@ -29,7 +29,7 @@ import { isBinExpression } from '../../../sql/bin';
|
|
|
29
29
|
import { isRollingAvgExpression } from '../../../sql/rolling-avg';
|
|
30
30
|
import { isWindowExpression } from '../../../sql/expression-utils';
|
|
31
31
|
import { identityKeyFor } from '../../../query-engine/identity-key';
|
|
32
|
-
import { activityReads, groupIndexForResponse, NO_READS, readsFor, readsForResponse, rendererForMark } from '../../rendering/activity-reads';
|
|
32
|
+
import { activityReads, dimsBySelection, groupIndexForResponse, NO_READS, readsFor, readsForResponse, rendererForMark } from '../../rendering/activity-reads';
|
|
33
33
|
import { AdaptiveBinRung } from '../../../lod/AdaptiveBinRung.svelte';
|
|
34
34
|
/** A SQL identifier, quoted, with any double quote in it doubled. */
|
|
35
35
|
function ident(name) {
|
|
@@ -143,13 +143,10 @@ export class MarkLifecycleV2 {
|
|
|
143
143
|
// Resolve AutoBinNodes — reads #fieldStats ($state), #lastBinDomain ($state),
|
|
144
144
|
// binStepMultiplier (reactive on chartContext)
|
|
145
145
|
if (this.#autoBinNodes.size > 0 && this.#fieldStats) {
|
|
146
|
-
const binStepMultiplier = this.#chartContext.binStepMultiplier || 1.0;
|
|
147
146
|
for (const [ch, value] of Object.entries(resolvedSelect)) {
|
|
148
147
|
if (isBinExpression(value)) {
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
const currentDomain = this.#lastBinDomain[axis] ?? null;
|
|
152
|
-
resolvedSelect[ch] = value.resolve(this.#fieldStats, scaleType, null, currentDomain, binStepMultiplier);
|
|
148
|
+
const { fieldInfo, scaleType, currentDomain, multiplier } = this.#binArgs(ch);
|
|
149
|
+
resolvedSelect[ch] = value.resolve(fieldInfo, scaleType, null, currentDomain, multiplier);
|
|
153
150
|
}
|
|
154
151
|
}
|
|
155
152
|
}
|
|
@@ -189,8 +186,40 @@ export class MarkLifecycleV2 {
|
|
|
189
186
|
const nullDrops = this.#nullDrops(semanticSelect, resolvedSelect);
|
|
190
187
|
if (nullDrops.length > 0)
|
|
191
188
|
parts.nullDrops = nullDrops;
|
|
189
|
+
parts.bins = this.#binPadding();
|
|
192
190
|
return parts;
|
|
193
191
|
});
|
|
192
|
+
/**
|
|
193
|
+
* What a bin channel resolves with: the field's stats, the axis's scale
|
|
194
|
+
* type and last re-bin domain, and the chart's step multiplier. One place,
|
|
195
|
+
* so the bin expression and its padding cannot be resolved differently.
|
|
196
|
+
* Reactive reads, from the deriveds that call it.
|
|
197
|
+
*/
|
|
198
|
+
#binArgs(channel) {
|
|
199
|
+
const axis = channel.startsWith('x') ? 'x' : 'y';
|
|
200
|
+
return {
|
|
201
|
+
axis,
|
|
202
|
+
fieldInfo: this.#fieldStats,
|
|
203
|
+
scaleType: axis === 'x' ? this.actualXScaleType : this.actualYScaleType,
|
|
204
|
+
currentDomain: this.#lastBinDomain[axis] ?? null,
|
|
205
|
+
multiplier: this.#chartContext.binStepMultiplier || 1.0
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
/** The bins of each numeric bin channel, for dense() to pad from; null without any. */
|
|
209
|
+
#binPadding() {
|
|
210
|
+
if (this.#autoBinNodes.size === 0 || !this.#fieldStats)
|
|
211
|
+
return null;
|
|
212
|
+
let out = null;
|
|
213
|
+
for (const [ch, value] of Object.entries(this.#config.select)) {
|
|
214
|
+
if (!isBinExpression(value))
|
|
215
|
+
continue;
|
|
216
|
+
const { axis, fieldInfo, scaleType, currentDomain, multiplier } = this.#binArgs(ch);
|
|
217
|
+
const padding = value.padding(fieldInfo, scaleType, currentDomain, multiplier, this.#timeOrigin(axis));
|
|
218
|
+
if (padding)
|
|
219
|
+
(out ??= {})[ch] = padding;
|
|
220
|
+
}
|
|
221
|
+
return out;
|
|
222
|
+
}
|
|
194
223
|
/**
|
|
195
224
|
* The mark's categorical channels, each with the source expression it draws:
|
|
196
225
|
* a band or point axis, an ordinal colour, a separate series, and the facet
|
|
@@ -277,9 +306,27 @@ export class MarkLifecycleV2 {
|
|
|
277
306
|
// --- Tooltip ---
|
|
278
307
|
#tooltipTableName;
|
|
279
308
|
#tooltipTableReady = $state(false);
|
|
309
|
+
/**
|
|
310
|
+
* True once a tooltip table write has started, so there may be a table to
|
|
311
|
+
* drop. Without it every lifecycle torn down before its first data (a mark
|
|
312
|
+
* that unmounts early, a rebuild) sent DuckDB a DROP for a table that never
|
|
313
|
+
* existed, one more statement in the single worker's queue during load.
|
|
314
|
+
*/
|
|
315
|
+
#tooltipTableWritten = false;
|
|
280
316
|
#tooltipWriteTimeout = null;
|
|
281
317
|
// --- fitDomain ---
|
|
282
318
|
#fitDomainTimeout = null;
|
|
319
|
+
/**
|
|
320
|
+
* The data version whose fit snaps instead of springing: the version the
|
|
321
|
+
* chart came back into view with, or its catch-up result's. A spring means
|
|
322
|
+
* the user just caused the change; a return to view is not that. Tied to a
|
|
323
|
+
* version rather than a flag waiting for the next fit: a fit issued for any
|
|
324
|
+
* other reason (a driving-axis zoom, later data) or skipped clears it, and
|
|
325
|
+
* a superseded fit for the same version keeps it.
|
|
326
|
+
*/
|
|
327
|
+
#snapFitVersion = null;
|
|
328
|
+
/** Debounces a refit after a data change (#refitFromData). */
|
|
329
|
+
#refitTimeout = null;
|
|
283
330
|
#fitDomainLastTarget = [0, 0];
|
|
284
331
|
#fitDomainQuerySequence = 0;
|
|
285
332
|
#fitDomainSuppressed = false;
|
|
@@ -292,6 +339,14 @@ export class MarkLifecycleV2 {
|
|
|
292
339
|
this.#chartContext = chartContext;
|
|
293
340
|
this.#markId = markId;
|
|
294
341
|
this.#filterGroup = filterGroup;
|
|
342
|
+
// The chart's entrance waits for this mark's first data, unless the
|
|
343
|
+
// author has said not to (a mark known to be slow). Held by mark id, so
|
|
344
|
+
// a lifecycle rebuilt for the same mark holds the same hold. Released on
|
|
345
|
+
// first data, on failure, or when the mark unmounts.
|
|
346
|
+
this.#entranceGate = chartContext.config?.entrance ?? null;
|
|
347
|
+
if (config.table && untrack(() => reactiveConfig.holdsEntrance) !== false) {
|
|
348
|
+
this.#entranceGate?.hold(markId);
|
|
349
|
+
}
|
|
295
350
|
// Tooltip table name
|
|
296
351
|
this.#tooltipTableName = `_tt_${this.#markId.replace(/-/g, '_')}`;
|
|
297
352
|
// Transform compiler
|
|
@@ -327,6 +382,7 @@ export class MarkLifecycleV2 {
|
|
|
327
382
|
identity: () => this.#identity,
|
|
328
383
|
identityKey: (axis) => this.#identityKey(axis)?.key ?? null,
|
|
329
384
|
filtered: () => this.#reactiveConfig.selectionResponse === 'filter',
|
|
385
|
+
dims: () => dimsBySelection(this.#reactiveConfig.selectionResponse),
|
|
330
386
|
plan: () => this.#splitPlan,
|
|
331
387
|
colorKey: () => {
|
|
332
388
|
const color = this.#colorScaleState;
|
|
@@ -503,7 +559,20 @@ export class MarkLifecycleV2 {
|
|
|
503
559
|
const scalesReady = (!this.#config.select?.[colorKey] || !!this.#colorScaleState) &&
|
|
504
560
|
(!seriesKey || seriesKey === colorKey || !!this.#seriesScaleState);
|
|
505
561
|
untrack(() => {
|
|
506
|
-
|
|
562
|
+
// A run already in flight against these same scales would produce
|
|
563
|
+
// the same result: don't send its queries again. A different scale
|
|
564
|
+
// starts a new run, and the generation guard retires the old one.
|
|
565
|
+
const inputs = [
|
|
566
|
+
xScaleState,
|
|
567
|
+
yScaleState,
|
|
568
|
+
this.#colorScaleState,
|
|
569
|
+
this.#seriesScaleState,
|
|
570
|
+
scaleTypes.x,
|
|
571
|
+
scaleTypes.y
|
|
572
|
+
];
|
|
573
|
+
if (scalesReady && this.#setupInFlight && sameInputs(this.#setupInFlight, inputs))
|
|
574
|
+
return;
|
|
575
|
+
this.#runSetup(scalesReady, inputs);
|
|
507
576
|
});
|
|
508
577
|
});
|
|
509
578
|
// Registration: watches #resolvedQueryParts, registers with engine.
|
|
@@ -534,10 +603,37 @@ export class MarkLifecycleV2 {
|
|
|
534
603
|
parts,
|
|
535
604
|
schema,
|
|
536
605
|
transformCompiler: this.#transformCompiler,
|
|
537
|
-
route: this.#route
|
|
606
|
+
route: this.#route,
|
|
607
|
+
// holdsEntrance={false}: the author knows this mark is slow.
|
|
608
|
+
slow: this.#reactiveConfig.holdsEntrance === false
|
|
538
609
|
}, this.#filterGroup);
|
|
539
610
|
});
|
|
540
611
|
});
|
|
612
|
+
// Off screen, the engine stops re-querying this mark (after its first
|
|
613
|
+
// result) and queries it once when it is back; see MarkHandle.active.
|
|
614
|
+
//
|
|
615
|
+
// The one owner of a return to view. A fitted axis may have moved
|
|
616
|
+
// meanwhile (a shared axis zoomed from another chart), and the fit
|
|
617
|
+
// effects do not run off screen. If the mark came back stale, its
|
|
618
|
+
// catch-up result refits like any data change (#refitFromData, or
|
|
619
|
+
// #onTooltipTableWritten with JS transforms). Refitting here as well
|
|
620
|
+
// would fit to the old data first, then to the new: two fits.
|
|
621
|
+
// Otherwise its data is current and nothing else will refit, so
|
|
622
|
+
// refit now.
|
|
623
|
+
$effect(() => {
|
|
624
|
+
const handle = this.#handle;
|
|
625
|
+
const visible = this.#chartContext.visible ?? true;
|
|
626
|
+
if (!handle)
|
|
627
|
+
return;
|
|
628
|
+
untrack(() => {
|
|
629
|
+
const cameBack = visible && !handle.active;
|
|
630
|
+
const caughtUp = handle.setActive(visible);
|
|
631
|
+
if (!visible)
|
|
632
|
+
this.#snapFitVersion = null;
|
|
633
|
+
if (cameBack && !caughtUp)
|
|
634
|
+
this.#refitOnReturn();
|
|
635
|
+
});
|
|
636
|
+
});
|
|
541
637
|
// Push per-mark params and trigger query. Runs immediately after
|
|
542
638
|
// registration (Svelte flushes effects in creation order), providing
|
|
543
639
|
// the initial query. On subsequent changes to sort/limit,
|
|
@@ -573,8 +669,16 @@ export class MarkLifecycleV2 {
|
|
|
573
669
|
untrack(() => {
|
|
574
670
|
this.#handleDataDomains(dv);
|
|
575
671
|
this.otherRows.register();
|
|
672
|
+
// After the domains: the last release opens the gate, and the
|
|
673
|
+
// scales snap to every mark's extent only until then.
|
|
674
|
+
this.#releaseEntrance();
|
|
576
675
|
});
|
|
577
676
|
});
|
|
677
|
+
// A failed query has nothing to enter with; it must not hold the chart.
|
|
678
|
+
$effect(() => {
|
|
679
|
+
if (this.#handle?.error)
|
|
680
|
+
untrack(() => this.#releaseEntrance());
|
|
681
|
+
});
|
|
578
682
|
// Tooltip: two effects, so a data update does not take the table away.
|
|
579
683
|
//
|
|
580
684
|
// This one owns the table's lifetime: it drops it when tooltips go from
|
|
@@ -592,11 +696,24 @@ export class MarkLifecycleV2 {
|
|
|
592
696
|
return () => {
|
|
593
697
|
clearTimeout(this.#tooltipWriteTimeout);
|
|
594
698
|
this.#tooltipTableReady = false;
|
|
595
|
-
this.#
|
|
699
|
+
this.#dropTooltipTable();
|
|
596
700
|
};
|
|
597
701
|
});
|
|
598
|
-
// And this one rewrites it when data updates
|
|
599
|
-
//
|
|
702
|
+
// And this one rewrites it when data updates, but only when someone may
|
|
703
|
+
// hover it. The table is a second scan of the mark's data, often with a
|
|
704
|
+
// sort, and nobody hovers most charts after most changes (at load, or
|
|
705
|
+
// while another chart is brushed), so a write per data change was the
|
|
706
|
+
// largest cost after a table load.
|
|
707
|
+
//
|
|
708
|
+
// - The chart hovered: written after the data settles (debounced), as
|
|
709
|
+
// before. The old table stays readable until the rewrite replaces it,
|
|
710
|
+
// so a click right after a data update still finds its row.
|
|
711
|
+
// - Not hovered: the table is marked stale (not ready), which takes the
|
|
712
|
+
// mark out of the tooltip manager, so no lookup reads old rows. It is
|
|
713
|
+
// written when the pointer enters the chart, at once, as interaction:
|
|
714
|
+
// it is what the first hover waits for.
|
|
715
|
+
// - fitDomain on a mark with JS transforms reads the table to fit
|
|
716
|
+
// (#buildFitDomainQuery), so that mark writes eagerly, in background.
|
|
600
717
|
$effect(() => {
|
|
601
718
|
const tooltipConfig = this.#reactiveConfig.tooltip;
|
|
602
719
|
const fitDomain = this.#reactiveConfig.fitDomain;
|
|
@@ -605,16 +722,71 @@ export class MarkLifecycleV2 {
|
|
|
605
722
|
const dataVersion = this.dataVersion;
|
|
606
723
|
if (dataVersion === 0)
|
|
607
724
|
return;
|
|
725
|
+
const hovered = this.#chartContext.hovered ?? true;
|
|
726
|
+
const eager = !!fitDomain && this.#transformCompiler.hasJSTransforms();
|
|
608
727
|
untrack(() => {
|
|
728
|
+
if (dataVersion === this.#tooltipWrittenFor)
|
|
729
|
+
return;
|
|
609
730
|
clearTimeout(this.#tooltipWriteTimeout);
|
|
610
|
-
|
|
731
|
+
// A data change waits for the data to settle; hover arriving at a
|
|
732
|
+
// stale table writes at once.
|
|
733
|
+
const dataChanged = dataVersion !== this.#tooltipSeenVersion;
|
|
734
|
+
this.#tooltipSeenVersion = dataVersion;
|
|
735
|
+
if (!hovered && !eager) {
|
|
736
|
+
this.#tooltipTableReady = false;
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
const cls = eager && !hovered ? 'background' : 'interaction';
|
|
740
|
+
const write = () => {
|
|
611
741
|
const data = this.data;
|
|
612
|
-
if (data
|
|
613
|
-
|
|
614
|
-
|
|
742
|
+
if (!data || data.length === 0)
|
|
743
|
+
return;
|
|
744
|
+
this.#tooltipWrittenFor = dataVersion;
|
|
745
|
+
this.#writeTooltipTable(data, cls);
|
|
746
|
+
};
|
|
747
|
+
// Sent from this effect, not a timer, so it goes out before a preagg
|
|
748
|
+
// build the same pointer enter queues (see Preagg.activate).
|
|
749
|
+
// A catch-up result is one result, with nothing to settle.
|
|
750
|
+
const catchUp = this.#handle?.resultCause === 'catchUp';
|
|
751
|
+
if (dataChanged && !catchUp)
|
|
752
|
+
this.#tooltipWriteTimeout = setTimeout(write, 300);
|
|
753
|
+
else
|
|
754
|
+
write();
|
|
615
755
|
});
|
|
616
756
|
return () => clearTimeout(this.#tooltipWriteTimeout);
|
|
617
757
|
});
|
|
758
|
+
// fitDomain after a data change (a brush on another chart, the first
|
|
759
|
+
// result): refit, debounced like the data settling. Marks with JS
|
|
760
|
+
// transforms refit after their tooltip table is written instead, which
|
|
761
|
+
// their fit reads (#onTooltipTableWritten). Off screen, like the mark's
|
|
762
|
+
// re-queries: the return to view refits (#refitOnReturn).
|
|
763
|
+
$effect(() => {
|
|
764
|
+
const fitDomain = this.#reactiveConfig.fitDomain;
|
|
765
|
+
if (!fitDomain)
|
|
766
|
+
return;
|
|
767
|
+
const dataVersion = this.dataVersion;
|
|
768
|
+
if (dataVersion === 0)
|
|
769
|
+
return;
|
|
770
|
+
// Untracked: a return to view refits from the setActive effect, which
|
|
771
|
+
// knows whether a catch-up result is coming.
|
|
772
|
+
if (!untrack(() => this.#chartContext.visible ?? true))
|
|
773
|
+
return;
|
|
774
|
+
// A catch-up result's fit snaps, and has no data to wait for.
|
|
775
|
+
const catchUp = untrack(() => this.#handle?.resultCause === 'catchUp');
|
|
776
|
+
this.#snapFitVersion = catchUp ? dataVersion : null;
|
|
777
|
+
// With JS transforms the fit reads the tooltip table, and a data change
|
|
778
|
+
// refits after its write (#onTooltipTableWritten).
|
|
779
|
+
if (this.#transformCompiler.hasJSTransforms())
|
|
780
|
+
return;
|
|
781
|
+
untrack(() => {
|
|
782
|
+
clearTimeout(this.#refitTimeout);
|
|
783
|
+
if (catchUp)
|
|
784
|
+
this.#refitFromData();
|
|
785
|
+
else
|
|
786
|
+
this.#refitTimeout = setTimeout(() => this.#refitFromData(), 300);
|
|
787
|
+
});
|
|
788
|
+
return () => clearTimeout(this.#refitTimeout);
|
|
789
|
+
});
|
|
618
790
|
// fitDomain — query DuckDB for fitted axis extent on driving axis change.
|
|
619
791
|
$effect(() => {
|
|
620
792
|
const fitDomain = this.#reactiveConfig.fitDomain;
|
|
@@ -635,6 +807,16 @@ export class MarkLifecycleV2 {
|
|
|
635
807
|
const fittedSS = fittedAxis === 'x' ? this.#chartContext.xScaleState : this.#chartContext.yScaleState;
|
|
636
808
|
const fittedUserDriven = fittedSS?.userDriven ?? false;
|
|
637
809
|
const drivingUserDriven = drivingSS.userDriven;
|
|
810
|
+
// Off screen, like the mark's re-queries (MarkHandle.active): no fit.
|
|
811
|
+
// Read untracked: the return to view fits once (#refitOnReturn, or
|
|
812
|
+
// the catch-up result), at the driving axis's domain then; tracking
|
|
813
|
+
// it here too sent a second fit for the same return. Checked only
|
|
814
|
+
// AFTER the driving domain is read: an effect re-runs on what its last
|
|
815
|
+
// run read, so returning before that left the effect deaf to the
|
|
816
|
+
// driving axis after any run off screen, and brushes made once the
|
|
817
|
+
// chart was back on screen sent no fit at all.
|
|
818
|
+
if (!untrack(() => this.#chartContext.visible ?? true))
|
|
819
|
+
return;
|
|
638
820
|
if (fittedSS) {
|
|
639
821
|
fittedSS.fitDomainActive = true;
|
|
640
822
|
}
|
|
@@ -645,6 +827,9 @@ export class MarkLifecycleV2 {
|
|
|
645
827
|
if (this.#fitDomainSuppressed && !drivingUserDriven) {
|
|
646
828
|
this.#fitDomainSuppressed = false;
|
|
647
829
|
}
|
|
830
|
+
// The driving axis moved: whatever fit follows is that zoom's, and
|
|
831
|
+
// springs.
|
|
832
|
+
this.#snapFitVersion = null;
|
|
648
833
|
if (this.#fitDomainSuppressed)
|
|
649
834
|
return;
|
|
650
835
|
clearTimeout(this.#fitDomainTimeout);
|
|
@@ -657,7 +842,11 @@ export class MarkLifecycleV2 {
|
|
|
657
842
|
// ===========================================================================
|
|
658
843
|
// PHASE 1: SETUP
|
|
659
844
|
// ===========================================================================
|
|
660
|
-
|
|
845
|
+
/** Bumped by each setup run that queries; see #runSetup. */
|
|
846
|
+
#setupGeneration = 0;
|
|
847
|
+
/** The scales the latest querying setup run started with, while it is in flight. */
|
|
848
|
+
#setupInFlight = null;
|
|
849
|
+
async #runSetup(colorReady, inputs) {
|
|
661
850
|
if (this.#destroyed)
|
|
662
851
|
return;
|
|
663
852
|
// Wait for the colour scale — and the series scale, which takes a dim the
|
|
@@ -693,9 +882,30 @@ export class MarkLifecycleV2 {
|
|
|
693
882
|
// a chart that was never going to get a colour scale.
|
|
694
883
|
if (!colorReady)
|
|
695
884
|
return;
|
|
885
|
+
// The setup effect re-enters whenever a scale it reads settles, and an
|
|
886
|
+
// earlier run may still be awaiting its queries. Only the latest run
|
|
887
|
+
// may finish: a superseded one stops after its await, so it cannot
|
|
888
|
+
// register domains on a scale that has since been replaced, nor set
|
|
889
|
+
// setupComplete from stale scale state. Its queries are already sent.
|
|
890
|
+
const generation = ++this.#setupGeneration;
|
|
891
|
+
const superseded = () => this.#destroyed || generation !== this.#setupGeneration;
|
|
892
|
+
this.#setupInFlight = inputs;
|
|
893
|
+
try {
|
|
894
|
+
await this.#runSetupQueries(superseded);
|
|
895
|
+
}
|
|
896
|
+
finally {
|
|
897
|
+
if (generation === this.#setupGeneration)
|
|
898
|
+
this.#setupInFlight = null;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
/** The querying part of #runSetup; `superseded` says a later run has started. */
|
|
902
|
+
async #runSetupQueries(superseded) {
|
|
696
903
|
// Passive scales: skip domain fetching but still need schema for CAST decisions
|
|
697
904
|
if (this.#config.passiveScales) {
|
|
698
|
-
|
|
905
|
+
const columns = await this.#queryEngine.describe(this.#config.table);
|
|
906
|
+
if (superseded())
|
|
907
|
+
return;
|
|
908
|
+
this.#schema = buildSchemaMap(columns);
|
|
699
909
|
setScalesReady(this.#scaleMap, this.#channelsNeedingDataDomain, this.actualXScaleType, this.actualYScaleType, false, false);
|
|
700
910
|
this.setupComplete = true;
|
|
701
911
|
return;
|
|
@@ -709,12 +919,12 @@ export class MarkLifecycleV2 {
|
|
|
709
919
|
const [schemaColumns, fetchCount, aggResult, fieldStats] = await Promise.all([
|
|
710
920
|
// Fetch table schema (cached on QueryEngine, needed for CAST type decisions)
|
|
711
921
|
this.#queryEngine.describe(this.#config.table),
|
|
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'),
|
|
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),
|
|
922
|
+
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', superseded),
|
|
923
|
+
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, superseded),
|
|
714
924
|
this.#fetchFieldStats(),
|
|
715
925
|
this.#ensureIdentityColumns()
|
|
716
926
|
]);
|
|
717
|
-
if (
|
|
927
|
+
if (superseded())
|
|
718
928
|
return;
|
|
719
929
|
this.#schema = buildSchemaMap(schemaColumns);
|
|
720
930
|
if (aggResult.globalAggregatesSet)
|
|
@@ -735,8 +945,15 @@ export class MarkLifecycleV2 {
|
|
|
735
945
|
if (error?.name === 'AbortError')
|
|
736
946
|
return;
|
|
737
947
|
console.error(`[MarkLifecycleV2] Setup failed for ${this.#markId}:`, error);
|
|
948
|
+
this.#releaseEntrance();
|
|
738
949
|
}
|
|
739
950
|
}
|
|
951
|
+
/** The chart's entrance gate, or null outside a chart (tests). */
|
|
952
|
+
#entranceGate = null;
|
|
953
|
+
/** Stop holding the chart's entrance. Idempotent: the gate ignores a second. */
|
|
954
|
+
#releaseEntrance() {
|
|
955
|
+
this.#entranceGate?.release(this.#markId);
|
|
956
|
+
}
|
|
740
957
|
// ===========================================================================
|
|
741
958
|
// QUERY HELPERS
|
|
742
959
|
// ===========================================================================
|
|
@@ -832,13 +1049,10 @@ export class MarkLifecycleV2 {
|
|
|
832
1049
|
['x', ['x', 'x1', 'x2']],
|
|
833
1050
|
['y', ['y', 'y1', 'y2']]
|
|
834
1051
|
]) {
|
|
835
|
-
const
|
|
836
|
-
if (
|
|
1052
|
+
const t0Epoch = this.#timeOrigin(axis);
|
|
1053
|
+
if (t0Epoch === null)
|
|
837
1054
|
continue;
|
|
838
1055
|
const scaleState = this.#chartContext[`${axis}ScaleState`];
|
|
839
|
-
if (!scaleState?.t0)
|
|
840
|
-
continue;
|
|
841
|
-
const t0Epoch = Math.floor(scaleState.t0.getTime() / 1000);
|
|
842
1056
|
for (const ch of channels) {
|
|
843
1057
|
if (!(ch in select))
|
|
844
1058
|
continue;
|
|
@@ -854,6 +1068,20 @@ export class MarkLifecycleV2 {
|
|
|
854
1068
|
}
|
|
855
1069
|
}
|
|
856
1070
|
}
|
|
1071
|
+
/**
|
|
1072
|
+
* The time axis's origin in epoch seconds, which its channels are drawn
|
|
1073
|
+
* relative to, or null when the axis is not a time scale or has no origin
|
|
1074
|
+
* yet. Its channels are rewritten, and its time buckets padded, only with one.
|
|
1075
|
+
*/
|
|
1076
|
+
#timeOrigin(axis) {
|
|
1077
|
+
const scaleType = axis === 'x' ? this.actualXScaleType : this.actualYScaleType;
|
|
1078
|
+
if (scaleType !== 'time')
|
|
1079
|
+
return null;
|
|
1080
|
+
const scaleState = this.#chartContext[`${axis}ScaleState`];
|
|
1081
|
+
if (!scaleState?.t0)
|
|
1082
|
+
return null;
|
|
1083
|
+
return Math.floor(scaleState.t0.getTime() / 1000);
|
|
1084
|
+
}
|
|
857
1085
|
/**
|
|
858
1086
|
* Fetch MIN/MAX stats for continuous scale fields.
|
|
859
1087
|
* Replaces Mosaic's queryFieldInfo (old Effect 5 fieldInfo path).
|
|
@@ -922,7 +1150,8 @@ export class MarkLifecycleV2 {
|
|
|
922
1150
|
}
|
|
923
1151
|
}
|
|
924
1152
|
const q = Query.from(this.#config.table).select(selectDef);
|
|
925
|
-
|
|
1153
|
+
// Shared and cached: marks on one table ask the same ranges (see QueryOpts.cache).
|
|
1154
|
+
const result = await this.#queryEngine.query(q, { cache: true, source: this.#config.table });
|
|
926
1155
|
const row = result.get(0);
|
|
927
1156
|
if (!row)
|
|
928
1157
|
return null;
|
|
@@ -969,8 +1198,7 @@ export class MarkLifecycleV2 {
|
|
|
969
1198
|
const additionalChannels = this.#transformCompiler.getAdditionalChannels();
|
|
970
1199
|
const xState = this.#chartContext.xScaleState;
|
|
971
1200
|
const yState = this.#chartContext.yScaleState;
|
|
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,
|
|
973
|
-
this.#colorScaleState);
|
|
1201
|
+
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, this.#instantFor, this.#colorScaleState);
|
|
974
1202
|
this.#dataDerivedDomainsSet = domainResult.dataDerivedDomainsSet;
|
|
975
1203
|
this.#skipDomainUpdateForAxis = domainResult.skipDomainUpdateForAxis;
|
|
976
1204
|
markDeferredScalesReady(this.#scaleMap, this.#channelsNeedingDataDomain, this.actualXScaleType, this.actualYScaleType);
|
|
@@ -1084,6 +1312,13 @@ export class MarkLifecycleV2 {
|
|
|
1084
1312
|
// the view. Failing here gives a message naming the mark and the fields;
|
|
1085
1313
|
// continuing would fail at the binder with neither.
|
|
1086
1314
|
await this.#queryEngine.ensureColumns(this.#config.table, { categorical });
|
|
1315
|
+
// Torn down while the dims were built: the scales below may have been
|
|
1316
|
+
// dropped with a ChartScales the chart has since replaced. Only a
|
|
1317
|
+
// destroyed lifecycle, not a superseded setup run: identity goes to the
|
|
1318
|
+
// scales read live from the chart context, so a live run handing it
|
|
1319
|
+
// over is harmless.
|
|
1320
|
+
if (this.#destroyed)
|
|
1321
|
+
return;
|
|
1087
1322
|
for (const { key } of categorical)
|
|
1088
1323
|
this.#identityFields.add(key);
|
|
1089
1324
|
// Tell the transforms which dim each categorical channel is identified by.
|
|
@@ -1330,6 +1565,11 @@ export class MarkLifecycleV2 {
|
|
|
1330
1565
|
* never mixed, the other axis's channels morph, and the band scale owns the
|
|
1331
1566
|
* clock both run on.
|
|
1332
1567
|
*
|
|
1568
|
+
* A histogram's binned axis aligns too: one row per bin, in bin order. Its
|
|
1569
|
+
* channels are bin edges rather than ids, still never mixed, and with no
|
|
1570
|
+
* slot clock on a linear scale the mark's channels run on a clock of their
|
|
1571
|
+
* own.
|
|
1572
|
+
*
|
|
1333
1573
|
* The first channel of the transforms' alignment — `dense()`'s plan, over the
|
|
1334
1574
|
* same select and facts its `apply()` sees — so the conditions are written
|
|
1335
1575
|
* once. Derived rather than recorded at apply time: apply runs for every
|
|
@@ -1343,10 +1583,13 @@ export class MarkLifecycleV2 {
|
|
|
1343
1583
|
void this.#identity;
|
|
1344
1584
|
// Asked exactly as assembleQuery asks when it orders the query, so this and
|
|
1345
1585
|
// the rows cannot disagree about which channel they are aligned on.
|
|
1346
|
-
const
|
|
1347
|
-
groupKeys: parts.groupKeys
|
|
1586
|
+
const aligned = this.#transformCompiler.alignment(Object.keys(parts.selectDef), {
|
|
1587
|
+
groupKeys: parts.groupKeys,
|
|
1588
|
+
bins: parts.bins ?? null
|
|
1348
1589
|
})?.[0];
|
|
1349
|
-
|
|
1590
|
+
// A band is `x` or `y`; a binned axis aligns on its lower edge, `x1`.
|
|
1591
|
+
const axis = aligned?.[0];
|
|
1592
|
+
return axis === 'x' || axis === 'y' ? axis : null;
|
|
1350
1593
|
});
|
|
1351
1594
|
/**
|
|
1352
1595
|
* Build a query for fetchGlobalAggregates.
|
|
@@ -1421,9 +1664,10 @@ export class MarkLifecycleV2 {
|
|
|
1421
1664
|
// --- Selection routing ---
|
|
1422
1665
|
/**
|
|
1423
1666
|
* The identity keys of the categorical fields this mark's renderer reads
|
|
1424
|
-
* selection activity for, sorted: its ordinal colour
|
|
1425
|
-
* activity-reads.ts says the renderer reads
|
|
1426
|
-
* one of these dims the mark; on any other
|
|
1667
|
+
* selection activity for, sorted: its ordinal colour, its band axes and a
|
|
1668
|
+
* line's or area's own `z`, as activity-reads.ts says the renderer reads
|
|
1669
|
+
* them. A categorical selection on one of these dims the mark; on any other
|
|
1670
|
+
* field it filters it.
|
|
1427
1671
|
*/
|
|
1428
1672
|
#reads = $derived.by(() => {
|
|
1429
1673
|
const renderer = rendererForMark(this.#config.markType);
|
|
@@ -1437,10 +1681,15 @@ export class MarkLifecycleV2 {
|
|
|
1437
1681
|
const band = this.#chartContext[`${axis}ScaleState`];
|
|
1438
1682
|
return band?.identityKey ?? null;
|
|
1439
1683
|
};
|
|
1684
|
+
// The series is a field of its own only on a separate channel (`z`);
|
|
1685
|
+
// without one, the series channel is the colour channel, read above.
|
|
1686
|
+
const seriesChannel = this.#config._orderConfig?.seriesChannel ?? null;
|
|
1687
|
+
const ownSeries = !!seriesChannel && seriesChannel !== (this.#config.colorChannel || 'fill');
|
|
1440
1688
|
return readsForResponse(this.#reactiveConfig.selectionResponse, readsFor(activityReads(renderer, this.#config.ruleExtent ?? false), {
|
|
1441
1689
|
color: color?.isOrdinal ? color.identityKey : null,
|
|
1442
1690
|
x: bandKey('x'),
|
|
1443
|
-
y: bandKey('y')
|
|
1691
|
+
y: bandKey('y'),
|
|
1692
|
+
series: ownSeries ? (this.#seriesScaleState?.identityKey ?? null) : null
|
|
1444
1693
|
}));
|
|
1445
1694
|
});
|
|
1446
1695
|
/** `#reads` as one string, so the registration effect re-runs only on a real change. */
|
|
@@ -1455,6 +1704,24 @@ export class MarkLifecycleV2 {
|
|
|
1455
1704
|
// NO_GROUP for selectionResponse 'filter', which never dims.
|
|
1456
1705
|
return groupIndexForResponse(this.#reactiveConfig.selectionResponse, this.#filterGroup.index);
|
|
1457
1706
|
}
|
|
1707
|
+
/**
|
|
1708
|
+
* The data version that lands without animating, or 0: a catch-up result
|
|
1709
|
+
* (the chart came back into view stale), a change nobody just made. A
|
|
1710
|
+
* version, not a flag: the cause stays set until the next result, and a
|
|
1711
|
+
* renderer lands only its FIRST write of this version. Later writes of the
|
|
1712
|
+
* same data (an Other split on a click, a JS transform's parameter) are
|
|
1713
|
+
* changes someone just made, and animate. Reactive through `dataVersion`,
|
|
1714
|
+
* which the cause is set with.
|
|
1715
|
+
*/
|
|
1716
|
+
get landsFor() {
|
|
1717
|
+
return this.#handle?.resultCause === 'catchUp' ? this.dataVersion : 0;
|
|
1718
|
+
}
|
|
1719
|
+
/**
|
|
1720
|
+
* Whether a scale's change from the current result lands at once: a
|
|
1721
|
+
* catch-up result, on a scale no other visible chart draws. Asked as the
|
|
1722
|
+
* result is applied, since visibility moves during a scroll.
|
|
1723
|
+
*/
|
|
1724
|
+
#instantFor = (scale) => this.#handle?.resultCause === 'catchUp' && !this.#chartContext.shownElsewhere(scale);
|
|
1458
1725
|
get dataVersion() {
|
|
1459
1726
|
return this.#handle?.dataVersion ?? 0;
|
|
1460
1727
|
}
|
|
@@ -1600,8 +1867,14 @@ export class MarkLifecycleV2 {
|
|
|
1600
1867
|
// ===========================================================================
|
|
1601
1868
|
// TOOLTIP WRITE-BACK
|
|
1602
1869
|
// ===========================================================================
|
|
1603
|
-
|
|
1870
|
+
/** The data version the tooltip table was last written for. */
|
|
1871
|
+
#tooltipWrittenFor = -1;
|
|
1872
|
+
/** The data version the write effect last saw, to tell a data change from a hover. */
|
|
1873
|
+
#tooltipSeenVersion = -1;
|
|
1874
|
+
async #writeTooltipTable(data, cls) {
|
|
1604
1875
|
const tableName = this.#tooltipTableName;
|
|
1876
|
+
// Before any statement: a write that fails part way can leave a table.
|
|
1877
|
+
this.#tooltipTableWritten = true;
|
|
1605
1878
|
try {
|
|
1606
1879
|
if (!this.#transformCompiler.hasJSTransforms()) {
|
|
1607
1880
|
// Path A: Build a query with original field names (not CAST-wrapped).
|
|
@@ -1644,11 +1917,17 @@ export class MarkLifecycleV2 {
|
|
|
1644
1917
|
replaceExprs.push(`CAST(${c} AS DOUBLE) AS ${c}`);
|
|
1645
1918
|
}
|
|
1646
1919
|
const replaceClause = replaceExprs.length > 0 ? ` REPLACE (${replaceExprs.join(', ')})` : '';
|
|
1647
|
-
//
|
|
1920
|
+
// ORDER BY x, so a hover lookup's bounding box could skip row groups
|
|
1921
|
+
// (zone maps): only when asked (`tooltip: { sortTable: true }`). Since
|
|
1922
|
+
// lazy writes the sort is hover latency, and the lookups did not get
|
|
1923
|
+
// faster for it. Measured on a scatter, sorted against unsorted:
|
|
1924
|
+
// 200k rows, write 41 against 12 ms, lookup median 2.2 against 2.9 ms;
|
|
1925
|
+
// 2M rows (17 row groups), write 273 against 70 ms, lookup median 11.3
|
|
1926
|
+
// against 11.4 ms, p90 35 against 30 ms. The lookup's box is wide and
|
|
1927
|
+
// holds most of the cost on dense data, sorted or not.
|
|
1648
1928
|
let orderClause = '';
|
|
1649
|
-
const tooltipSortTable = typeof this.#reactiveConfig.tooltip === 'object'
|
|
1650
|
-
|
|
1651
|
-
: true;
|
|
1929
|
+
const tooltipSortTable = typeof this.#reactiveConfig.tooltip === 'object' &&
|
|
1930
|
+
this.#reactiveConfig.tooltip.sortTable === true;
|
|
1652
1931
|
if (tooltipSortTable) {
|
|
1653
1932
|
const orderCol = ['x', 'x1', 'x2'].find((c) => c in selectConfig);
|
|
1654
1933
|
if (orderCol)
|
|
@@ -1661,7 +1940,7 @@ export class MarkLifecycleV2 {
|
|
|
1661
1940
|
// lists are the only ones whose size grows with the field.
|
|
1662
1941
|
const withoutMembers = withoutOtherMembers(tooltipQuery);
|
|
1663
1942
|
// 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}
|
|
1943
|
+
await this.#queryEngine.exec(`CREATE OR REPLACE TEMP TABLE "${tableName}" AS SELECT *${replaceClause} FROM (${withoutMembers})${orderClause}`, { class: cls, connectionLocal: true });
|
|
1665
1944
|
}
|
|
1666
1945
|
else {
|
|
1667
1946
|
// Path B: Serialize transformed data via SQL VALUES.
|
|
@@ -1703,13 +1982,13 @@ export class MarkLifecycleV2 {
|
|
|
1703
1982
|
`DROP TABLE IF EXISTS "${tableName}"`,
|
|
1704
1983
|
`CREATE TEMP TABLE "${tableName}" (${schemaDef})`,
|
|
1705
1984
|
`INSERT INTO "${tableName}" VALUES ${valueRows.join(', ')}`
|
|
1706
|
-
]);
|
|
1985
|
+
], { class: cls, connectionLocal: true });
|
|
1707
1986
|
}
|
|
1708
1987
|
// Merge tip expression extras (aggregates/windows) via sidecar query.
|
|
1709
1988
|
const tipExtras = this.#getTipExtras();
|
|
1710
1989
|
if (Object.keys(tipExtras).length > 0) {
|
|
1711
1990
|
try {
|
|
1712
|
-
await this.#mergeTipExtras(tableName, tipExtras);
|
|
1991
|
+
await this.#mergeTipExtras(tableName, tipExtras, cls);
|
|
1713
1992
|
}
|
|
1714
1993
|
catch (err) {
|
|
1715
1994
|
console.warn('Failed to merge tip extras into tooltip table:', err);
|
|
@@ -1772,7 +2051,7 @@ export class MarkLifecycleV2 {
|
|
|
1772
2051
|
* DB query. Computes aggregates against the source table using the same
|
|
1773
2052
|
* GROUP BY as the main query, then ALTER+UPDATE the temp table.
|
|
1774
2053
|
*/
|
|
1775
|
-
async #mergeTipExtras(tableName, tipExtras) {
|
|
2054
|
+
async #mergeTipExtras(tableName, tipExtras, cls) {
|
|
1776
2055
|
if (!this.#resolvedQueryParts)
|
|
1777
2056
|
return;
|
|
1778
2057
|
// Identify non-aggregate channel columns — these are the join keys
|
|
@@ -1810,7 +2089,10 @@ export class MarkLifecycleV2 {
|
|
|
1810
2089
|
.map((ch) => `${ident(tableName)}.${ident(ch)} = _s.${ident(ch)}`)
|
|
1811
2090
|
.join(' AND ');
|
|
1812
2091
|
const updateStmt = `UPDATE ${ident(tableName)} SET ${setClauses} FROM (${sidecarQuery.toString()}) _s WHERE ${joinCondition}`;
|
|
1813
|
-
await this.#queryEngine.exec([...alterStmts, updateStmt]
|
|
2092
|
+
await this.#queryEngine.exec([...alterStmts, updateStmt], {
|
|
2093
|
+
class: cls,
|
|
2094
|
+
connectionLocal: true
|
|
2095
|
+
});
|
|
1814
2096
|
}
|
|
1815
2097
|
// ===========================================================================
|
|
1816
2098
|
// FIT DOMAIN
|
|
@@ -1843,7 +2125,9 @@ export class MarkLifecycleV2 {
|
|
|
1843
2125
|
}
|
|
1844
2126
|
return channels;
|
|
1845
2127
|
}
|
|
1846
|
-
async #issueFitDomainQuery(fittedAxis, drivingDomain, force = false
|
|
2128
|
+
async #issueFitDomainQuery(fittedAxis, drivingDomain, force = false,
|
|
2129
|
+
/** A return-to-view fit (#snapFitVersion): lands without a spring. */
|
|
2130
|
+
snap = false) {
|
|
1847
2131
|
if (!force && drivingDomain) {
|
|
1848
2132
|
const [lastLo, lastHi] = this.#fitDomainLastTarget;
|
|
1849
2133
|
const lastWidth = lastHi - lastLo;
|
|
@@ -1862,7 +2146,11 @@ export class MarkLifecycleV2 {
|
|
|
1862
2146
|
return;
|
|
1863
2147
|
const result = await this.#queryEngine.query(q.toString(), {
|
|
1864
2148
|
callerId: `${this.#markId}:fitDomain`,
|
|
1865
|
-
cache: false
|
|
2149
|
+
cache: false,
|
|
2150
|
+
// With JS transforms the fit reads the TEMP tooltip table;
|
|
2151
|
+
// otherwise the mark's base table, routed by it.
|
|
2152
|
+
connectionLocal: this.#transformCompiler.hasJSTransforms(),
|
|
2153
|
+
source: this.#config.table
|
|
1866
2154
|
});
|
|
1867
2155
|
if (seq !== this.#fitDomainQuerySequence)
|
|
1868
2156
|
return;
|
|
@@ -1887,11 +2175,14 @@ export class MarkLifecycleV2 {
|
|
|
1887
2175
|
}
|
|
1888
2176
|
const scaleState = fittedAxis === 'x' ? this.#chartContext.xScaleState : this.#chartContext.yScaleState;
|
|
1889
2177
|
const scaleType = fittedAxis === 'x' ? this.actualXScaleType : this.actualYScaleType;
|
|
2178
|
+
// Asked as it lands: a chart in view that shares the axis keeps the
|
|
2179
|
+
// spring, and everything on the axis moves together through it.
|
|
2180
|
+
const instant = snap && !!scaleState && !this.#chartContext.shownElsewhere(scaleState);
|
|
1890
2181
|
if (scaleType === 'time' && scaleState && 'addOffsetExtent' in scaleState) {
|
|
1891
|
-
scaleState.addOffsetExtent([lo, hi], this.#markId);
|
|
2182
|
+
scaleState.addOffsetExtent([lo, hi], this.#markId, { instant });
|
|
1892
2183
|
}
|
|
1893
2184
|
else if (scaleState && 'addMark' in scaleState) {
|
|
1894
|
-
scaleState.addMark([lo, hi], this.#markId);
|
|
2185
|
+
scaleState.addMark([lo, hi], this.#markId, { instant });
|
|
1895
2186
|
}
|
|
1896
2187
|
if (drivingDomain)
|
|
1897
2188
|
this.#fitDomainLastTarget = [...drivingDomain];
|
|
@@ -1958,15 +2249,32 @@ export class MarkLifecycleV2 {
|
|
|
1958
2249
|
return q;
|
|
1959
2250
|
}
|
|
1960
2251
|
/**
|
|
1961
|
-
*
|
|
1962
|
-
*
|
|
2252
|
+
* A tooltip table was written. A fit over a mark with JS transforms reads that
|
|
2253
|
+
* table (#buildFitDomainQuery), so its refit after a data change waits for
|
|
2254
|
+
* the write; every other fit refits from the data itself (#refitFromData).
|
|
1963
2255
|
*/
|
|
1964
2256
|
#onTooltipTableWritten() {
|
|
2257
|
+
if (this.#transformCompiler.hasJSTransforms())
|
|
2258
|
+
this.#refitFromData();
|
|
2259
|
+
}
|
|
2260
|
+
/**
|
|
2261
|
+
* Refit the fitted axis to the mark's current data, at the driving axis's
|
|
2262
|
+
* current domain. The data-driven trigger: the fitDomain effect reacts to the
|
|
2263
|
+
* driving axis, not to new data, and returns for a band driving axis, so this
|
|
2264
|
+
* is also a band-driven fit's only trigger, its first fit included.
|
|
2265
|
+
*/
|
|
2266
|
+
#refitFromData() {
|
|
1965
2267
|
const fitDomain = this.#reactiveConfig.fitDomain;
|
|
1966
2268
|
if (!fitDomain)
|
|
1967
2269
|
return;
|
|
2270
|
+
// The fit for the snap's version, if this is it; a refit for any other
|
|
2271
|
+
// version springs, and a skipped one gives the snap up.
|
|
2272
|
+
const snap = this.#snapFitVersion !== null && this.#snapFitVersion === this.dataVersion;
|
|
2273
|
+
this.#snapFitVersion = null;
|
|
1968
2274
|
if (this.#fitDomainSuppressed)
|
|
1969
2275
|
return;
|
|
2276
|
+
if (snap)
|
|
2277
|
+
this.#snapFitVersion = this.dataVersion;
|
|
1970
2278
|
const fittedAxis = this.#resolveFittedAxis(fitDomain);
|
|
1971
2279
|
if (!fittedAxis)
|
|
1972
2280
|
return;
|
|
@@ -1975,19 +2283,37 @@ export class MarkLifecycleV2 {
|
|
|
1975
2283
|
if (!drivingSS)
|
|
1976
2284
|
return;
|
|
1977
2285
|
if (drivingSS.scaleType === 'band') {
|
|
1978
|
-
this.#issueFitDomainQuery(fittedAxis, null, true);
|
|
2286
|
+
this.#issueFitDomainQuery(fittedAxis, null, true, snap);
|
|
1979
2287
|
return;
|
|
1980
2288
|
}
|
|
1981
2289
|
const target = drivingSS.domain?.target;
|
|
1982
2290
|
if (!target)
|
|
1983
2291
|
return;
|
|
1984
|
-
this.#issueFitDomainQuery(fittedAxis, target, true);
|
|
2292
|
+
this.#issueFitDomainQuery(fittedAxis, target, true, snap);
|
|
2293
|
+
}
|
|
2294
|
+
/**
|
|
2295
|
+
* The chart came back into view with current data: fit to the driving
|
|
2296
|
+
* axis as it is now. Not debounced, since no data is settling. With JS
|
|
2297
|
+
* transforms the fit reads the tooltip table; if its write is still
|
|
2298
|
+
* pending, #onTooltipTableWritten fits after it instead.
|
|
2299
|
+
*/
|
|
2300
|
+
#refitOnReturn() {
|
|
2301
|
+
if (!this.#reactiveConfig.fitDomain || this.dataVersion === 0)
|
|
2302
|
+
return;
|
|
2303
|
+
this.#snapFitVersion = this.dataVersion;
|
|
2304
|
+
if (this.#transformCompiler.hasJSTransforms() && !this.#tooltipTableReady)
|
|
2305
|
+
return;
|
|
2306
|
+
clearTimeout(this.#refitTimeout);
|
|
2307
|
+
this.#refitFromData();
|
|
1985
2308
|
}
|
|
1986
2309
|
// ===========================================================================
|
|
1987
2310
|
// CLEANUP
|
|
1988
2311
|
// ===========================================================================
|
|
1989
2312
|
destroy() {
|
|
1990
2313
|
this.#destroyed = true;
|
|
2314
|
+
// Not releasing the entrance here: a mark rebuilds its lifecycle when
|
|
2315
|
+
// its scales resolve, and the rebuilt one re-holds the same id. The
|
|
2316
|
+
// mark's unmount releases it (ChartConfig.unregisterMark).
|
|
1991
2317
|
this.#identityResetOff?.();
|
|
1992
2318
|
this.#identityResetOff = null;
|
|
1993
2319
|
this.otherRows.unregister();
|
|
@@ -1995,6 +2321,7 @@ export class MarkLifecycleV2 {
|
|
|
1995
2321
|
this.#releaseOther(scale);
|
|
1996
2322
|
this.#handle?.destroy();
|
|
1997
2323
|
clearTimeout(this.#fitDomainTimeout);
|
|
2324
|
+
clearTimeout(this.#refitTimeout);
|
|
1998
2325
|
clearTimeout(this.#tooltipWriteTimeout);
|
|
1999
2326
|
if (this.#onMarkLOD) {
|
|
2000
2327
|
this.#chartContext.config.unregisterMarkLOD(this.#markId);
|
|
@@ -2010,6 +2337,23 @@ export class MarkLifecycleV2 {
|
|
|
2010
2337
|
// where destroy() is called synchronously before Svelte processes
|
|
2011
2338
|
// effect teardown.
|
|
2012
2339
|
this.#tooltipTableReady = false;
|
|
2013
|
-
this.#
|
|
2340
|
+
this.#dropTooltipTable();
|
|
2341
|
+
}
|
|
2342
|
+
#dropTooltipTable() {
|
|
2343
|
+
if (!this.#tooltipTableWritten)
|
|
2344
|
+
return;
|
|
2345
|
+
this.#tooltipTableWritten = false;
|
|
2346
|
+
this.#queryEngine
|
|
2347
|
+
.exec(`DROP TABLE IF EXISTS "${this.#tooltipTableName}"`, {
|
|
2348
|
+
class: 'background',
|
|
2349
|
+
// A TEMP table on the main connection, written and dropped only
|
|
2350
|
+
// there, so a drop cannot overtake the next lifecycle's write.
|
|
2351
|
+
connectionLocal: true
|
|
2352
|
+
})
|
|
2353
|
+
.catch(() => { });
|
|
2014
2354
|
}
|
|
2015
2355
|
}
|
|
2356
|
+
/** Same scale states and types, compared by identity. */
|
|
2357
|
+
function sameInputs(a, b) {
|
|
2358
|
+
return a.length === b.length && a.every((v, i) => v === b[i]);
|
|
2359
|
+
}
|