@fjader/svelte 0.2.3 → 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 +3 -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 +27 -8
- package/dist/components/axes/AxisBottom.svelte.d.ts +10 -0
- package/dist/components/axes/AxisCenter.svelte +4 -2
- package/dist/components/axes/AxisLeft.svelte +81 -10
- package/dist/components/axes/AxisLeft.svelte.d.ts +17 -0
- package/dist/components/axes/BandAxisTicks.svelte +4 -3
- 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 +33 -6
- package/dist/components/charts/ChartCell.svelte.d.ts +9 -0
- package/dist/components/charts/ChartContainer.svelte +55 -5
- package/dist/components/charts/ChartContainer.svelte.d.ts +14 -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/guides/legend/ContinuousColorLegend.svelte +7 -1
- package/dist/components/guides/legend/OrdinalColorLegend.svelte +8 -5
- package/dist/components/marks/AreaMarkX.svelte +18 -4
- package/dist/components/marks/AreaMarkY.svelte +18 -4
- package/dist/components/marks/BarMarkX.svelte +20 -4
- package/dist/components/marks/BarMarkY.svelte +20 -4
- package/dist/components/marks/DotsMark.svelte +8 -3
- package/dist/components/marks/LineMark.svelte +13 -6
- package/dist/components/marks/ParallelCoordinatesMark.svelte +11 -2
- package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +4 -0
- package/dist/components/marks/RectMark.svelte +8 -3
- package/dist/components/marks/RectXMark.svelte +9 -3
- package/dist/components/marks/RectYMark.svelte +9 -3
- package/dist/components/marks/RuleMarkX.svelte +31 -25
- package/dist/components/marks/RuleMarkY.svelte +31 -25
- package/dist/components/marks/TextMark.svelte +52 -14
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +22 -1
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +579 -66
- package/dist/components/marks/lifecycle/domain-queries.d.ts +13 -2
- package/dist/components/marks/lifecycle/domain-queries.js +43 -11
- package/dist/components/marks/lifecycle/domain-registration.d.ts +6 -1
- package/dist/components/marks/lifecycle/domain-registration.js +17 -20
- package/dist/components/marks/lifecycle/other-rows.svelte.d.ts +112 -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 +53 -3
- package/dist/components/marks/lifecycle/query-builder.js +46 -23
- 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 +9 -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/Areas.svelte +57 -19
- package/dist/components/rendering/Areas.svelte.d.ts +12 -0
- package/dist/components/rendering/BaseGPURenderer.svelte.d.ts +3 -1
- package/dist/components/rendering/BaseGPURenderer.svelte.js +4 -2
- 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 +164 -30
- package/dist/components/rendering/Rects.svelte.d.ts +23 -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 +47 -12
- package/dist/components/rendering/activity-reads.js +61 -19
- 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 +10 -1
- 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 -28
- package/dist/gpu/MorphedChannels.js +72 -8
- package/dist/gpu/ScaleTemplateSystem.d.ts +20 -2
- package/dist/gpu/ScaleTemplateSystem.js +83 -6
- package/dist/index.d.ts +19 -4
- package/dist/index.js +22 -4
- 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 +46 -12
- package/dist/interactors/other-tooltip.d.ts +16 -0
- package/dist/interactors/other-tooltip.js +22 -0
- 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 +49 -1
- package/dist/query-engine/FieldActivity.svelte.js +175 -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/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 +697 -47
- package/dist/query-engine/categorical-identity.d.ts +41 -0
- package/dist/query-engine/categorical-identity.js +145 -65
- 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 +33 -1
- package/dist/query-engine/preagg/plan.js +158 -14
- 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/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 +22 -9
- package/dist/scales/BandScaleState.svelte.js +39 -21
- 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 +43 -13
- package/dist/sql/bin.d.ts +25 -0
- package/dist/sql/bin.js +108 -5
- package/dist/theme.css +5 -4
- package/dist/transforms/TransformCompiler.d.ts +47 -2
- package/dist/transforms/TransformCompiler.js +70 -1
- package/dist/transforms/index.js +176 -42
- package/dist/types.d.ts +177 -2
- package/package.json +4 -3
|
@@ -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) {
|
|
@@ -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
|
// ---------------------------------------------------------------------------
|
|
@@ -142,13 +143,10 @@ export class MarkLifecycleV2 {
|
|
|
142
143
|
// Resolve AutoBinNodes — reads #fieldStats ($state), #lastBinDomain ($state),
|
|
143
144
|
// binStepMultiplier (reactive on chartContext)
|
|
144
145
|
if (this.#autoBinNodes.size > 0 && this.#fieldStats) {
|
|
145
|
-
const binStepMultiplier = this.#chartContext.binStepMultiplier || 1.0;
|
|
146
146
|
for (const [ch, value] of Object.entries(resolvedSelect)) {
|
|
147
147
|
if (isBinExpression(value)) {
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
const currentDomain = this.#lastBinDomain[axis] ?? null;
|
|
151
|
-
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);
|
|
152
150
|
}
|
|
153
151
|
}
|
|
154
152
|
}
|
|
@@ -185,8 +183,100 @@ export class MarkLifecycleV2 {
|
|
|
185
183
|
this.#inlineIdentityColumns(resolvedSelect);
|
|
186
184
|
this.#inlineTimeOffsets(resolvedSelect);
|
|
187
185
|
const parts = buildQueryParts(resolvedSelect, this.#config._orderConfig ?? null, this.#transformCompiler.hasSQLTransforms(), passthroughChannels);
|
|
186
|
+
const nullDrops = this.#nullDrops(semanticSelect, resolvedSelect);
|
|
187
|
+
if (nullDrops.length > 0)
|
|
188
|
+
parts.nullDrops = nullDrops;
|
|
189
|
+
parts.bins = this.#binPadding();
|
|
188
190
|
return parts;
|
|
189
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
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* The mark's categorical channels, each with the source expression it draws:
|
|
225
|
+
* a band or point axis, an ordinal colour, a separate series, and the facet
|
|
226
|
+
* (`__facet`, from the chart's fx). What `nulls="drop"` filters.
|
|
227
|
+
*/
|
|
228
|
+
#categoricalChannels(select) {
|
|
229
|
+
const out = [];
|
|
230
|
+
const categorical = (type) => type === 'band' || type === 'point';
|
|
231
|
+
if (categorical(this.actualXScaleType))
|
|
232
|
+
out.push(['x', select.x]);
|
|
233
|
+
if (categorical(this.actualYScaleType))
|
|
234
|
+
out.push(['y', select.y]);
|
|
235
|
+
const colorChannel = this.#config.colorChannel || 'fill';
|
|
236
|
+
if (this.#colorScaleState?.isOrdinal)
|
|
237
|
+
out.push([colorChannel, select[colorChannel]]);
|
|
238
|
+
const seriesChannel = this.#config._orderConfig?.seriesChannel ?? null;
|
|
239
|
+
if (seriesChannel && seriesChannel !== colorChannel) {
|
|
240
|
+
out.push([seriesChannel, select[seriesChannel]]);
|
|
241
|
+
}
|
|
242
|
+
if (this.#chartContext.fx)
|
|
243
|
+
out.push(['__facet', this.#chartContext.fx]);
|
|
244
|
+
// Only row-level values: an aggregate has no NULL category to drop.
|
|
245
|
+
return out.filter(([, source]) => source != null &&
|
|
246
|
+
typeof source !== 'number' &&
|
|
247
|
+
!(isNode(source) && isAggregateExpression(source)));
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* `nulls="drop"`, by channel (see QueryParts.nullDrops). A channel the query
|
|
251
|
+
* selects as its dim's id column drops the NULL category's id; one still
|
|
252
|
+
* carrying its raw value (identity not landed yet) drops SQL NULL. Read off
|
|
253
|
+
* the select itself, so the two cannot disagree: inlining an id column is
|
|
254
|
+
* what changes the channel's expression.
|
|
255
|
+
*/
|
|
256
|
+
#nullDrops(semantic, resolved) {
|
|
257
|
+
if (this.#reactiveConfig.nulls !== 'drop')
|
|
258
|
+
return [];
|
|
259
|
+
const drops = [];
|
|
260
|
+
for (const [channel, source] of this.#categoricalChannels(semantic)) {
|
|
261
|
+
const encoded = channel === '__facet' ? '__facet' in resolved : resolved[channel] !== source;
|
|
262
|
+
if (channel === '__facet' && !encoded)
|
|
263
|
+
continue;
|
|
264
|
+
const key = encoded ? identityKeyFor(source)?.key : null;
|
|
265
|
+
const nullId = key ? this.#queryEngine.identityFor(key).nullId : null;
|
|
266
|
+
drops.push({ channel, nullId });
|
|
267
|
+
}
|
|
268
|
+
return drops;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* `nulls="drop"` for a query over the RAW select (no id columns, no
|
|
272
|
+
* `__facet`): `<source> IS NOT NULL` per categorical channel, the facet's
|
|
273
|
+
* on the chart's fx. Passed as that query's filter.
|
|
274
|
+
*/
|
|
275
|
+
#rawNullFilters(select) {
|
|
276
|
+
if (this.#reactiveConfig.nulls !== 'drop')
|
|
277
|
+
return [];
|
|
278
|
+
return this.#categoricalChannels(select).map(([, source]) => isNotNull((typeof source === 'string' ? sqlColumn(source) : source)));
|
|
279
|
+
}
|
|
190
280
|
// --- Domain State ---
|
|
191
281
|
#channelsNeedingDataDomain = new Set();
|
|
192
282
|
#dataDerivedDomainsSet = $state(false);
|
|
@@ -216,9 +306,27 @@ export class MarkLifecycleV2 {
|
|
|
216
306
|
// --- Tooltip ---
|
|
217
307
|
#tooltipTableName;
|
|
218
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;
|
|
219
316
|
#tooltipWriteTimeout = null;
|
|
220
317
|
// --- fitDomain ---
|
|
221
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;
|
|
222
330
|
#fitDomainLastTarget = [0, 0];
|
|
223
331
|
#fitDomainQuerySequence = 0;
|
|
224
332
|
#fitDomainSuppressed = false;
|
|
@@ -231,6 +339,14 @@ export class MarkLifecycleV2 {
|
|
|
231
339
|
this.#chartContext = chartContext;
|
|
232
340
|
this.#markId = markId;
|
|
233
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
|
+
}
|
|
234
350
|
// Tooltip table name
|
|
235
351
|
this.#tooltipTableName = `_tt_${this.#markId.replace(/-/g, '_')}`;
|
|
236
352
|
// Transform compiler
|
|
@@ -257,6 +373,26 @@ export class MarkLifecycleV2 {
|
|
|
257
373
|
// than rebuilding. With no sort it passes the query through.
|
|
258
374
|
this.#transformCompiler = new TransformCompiler(withRanking(withDenseCategories(config.transforms || [], config.dense !== false), () => this.#rankingSort), this.#transformConfig);
|
|
259
375
|
this.#detectAutoBins();
|
|
376
|
+
this.otherRows = new OtherRows({
|
|
377
|
+
queryEngine,
|
|
378
|
+
filterGroup,
|
|
379
|
+
markId,
|
|
380
|
+
table: () => this.#handle?.arrowTable ?? null,
|
|
381
|
+
dataVersion: () => this.dataVersion,
|
|
382
|
+
identity: () => this.#identity,
|
|
383
|
+
identityKey: (axis) => this.#identityKey(axis)?.key ?? null,
|
|
384
|
+
filtered: () => this.#reactiveConfig.selectionResponse === 'filter',
|
|
385
|
+
dims: () => dimsBySelection(this.#reactiveConfig.selectionResponse),
|
|
386
|
+
plan: () => this.#splitPlan,
|
|
387
|
+
colorKey: () => {
|
|
388
|
+
const color = this.#colorScaleState;
|
|
389
|
+
return color?.isOrdinal ? color.identityKey : null;
|
|
390
|
+
},
|
|
391
|
+
bandScaleKey: (axis) => {
|
|
392
|
+
const band = (axis === 'x' ? this.#chartContext.xScaleState : this.#chartContext.yScaleState);
|
|
393
|
+
return band?.identityKey ?? null;
|
|
394
|
+
}
|
|
395
|
+
});
|
|
260
396
|
// AdaptiveBinRung for binned marks (watches viewport domain, triggers rebin)
|
|
261
397
|
if (this.#autoBinNodes.size > 0 && options?.gpuCoordinator) {
|
|
262
398
|
const binAxes = [...this.#autoBinNodes.keys()].map((ch) => (ch.startsWith('x') ? 'x' : 'y'));
|
|
@@ -342,7 +478,8 @@ export class MarkLifecycleV2 {
|
|
|
342
478
|
return;
|
|
343
479
|
this.#identityFields.clear();
|
|
344
480
|
// The Other ids name rows of the dropped dims. Every holder lets go, so
|
|
345
|
-
// the scale acquires afresh when setup re-runs.
|
|
481
|
+
// the scale acquires afresh when setup re-runs; so do the registered rows.
|
|
482
|
+
this.otherRows.unregister();
|
|
346
483
|
for (const scale of [...this.#otherScales])
|
|
347
484
|
this.#releaseOther(scale);
|
|
348
485
|
// Every dim it names just went away — topN must not bucket into one.
|
|
@@ -422,7 +559,20 @@ export class MarkLifecycleV2 {
|
|
|
422
559
|
const scalesReady = (!this.#config.select?.[colorKey] || !!this.#colorScaleState) &&
|
|
423
560
|
(!seriesKey || seriesKey === colorKey || !!this.#seriesScaleState);
|
|
424
561
|
untrack(() => {
|
|
425
|
-
|
|
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);
|
|
426
576
|
});
|
|
427
577
|
});
|
|
428
578
|
// Registration: watches #resolvedQueryParts, registers with engine.
|
|
@@ -453,10 +603,37 @@ export class MarkLifecycleV2 {
|
|
|
453
603
|
parts,
|
|
454
604
|
schema,
|
|
455
605
|
transformCompiler: this.#transformCompiler,
|
|
456
|
-
route: this.#route
|
|
606
|
+
route: this.#route,
|
|
607
|
+
// holdsEntrance={false}: the author knows this mark is slow.
|
|
608
|
+
slow: this.#reactiveConfig.holdsEntrance === false
|
|
457
609
|
}, this.#filterGroup);
|
|
458
610
|
});
|
|
459
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
|
+
});
|
|
460
637
|
// Push per-mark params and trigger query. Runs immediately after
|
|
461
638
|
// registration (Svelte flushes effects in creation order), providing
|
|
462
639
|
// the initial query. On subsequent changes to sort/limit,
|
|
@@ -491,32 +668,124 @@ export class MarkLifecycleV2 {
|
|
|
491
668
|
return;
|
|
492
669
|
untrack(() => {
|
|
493
670
|
this.#handleDataDomains(dv);
|
|
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();
|
|
494
675
|
});
|
|
495
676
|
});
|
|
496
|
-
//
|
|
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
|
+
});
|
|
682
|
+
// Tooltip: two effects, so a data update does not take the table away.
|
|
683
|
+
//
|
|
684
|
+
// This one owns the table's lifetime: it drops it when tooltips go from
|
|
685
|
+
// wanted to not. It used to be one effect with the write below, and its
|
|
686
|
+
// cleanup then ran on every data update too: the table was dropped and
|
|
687
|
+
// marked not ready, the mark's tooltip unregistered, and the hovered row
|
|
688
|
+
// went with it. A click right after a data update found no row under the
|
|
689
|
+
// pointer, read as a click on empty space and cleared the selection, and
|
|
690
|
+
// the next click did nothing until the pointer moved.
|
|
691
|
+
$effect(() => {
|
|
692
|
+
const tooltipConfig = this.#reactiveConfig.tooltip;
|
|
693
|
+
const fitDomain = this.#reactiveConfig.fitDomain;
|
|
694
|
+
if (tooltipConfig === false && !fitDomain)
|
|
695
|
+
return;
|
|
696
|
+
return () => {
|
|
697
|
+
clearTimeout(this.#tooltipWriteTimeout);
|
|
698
|
+
this.#tooltipTableReady = false;
|
|
699
|
+
this.#dropTooltipTable();
|
|
700
|
+
};
|
|
701
|
+
});
|
|
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.
|
|
497
717
|
$effect(() => {
|
|
498
718
|
const tooltipConfig = this.#reactiveConfig.tooltip;
|
|
499
719
|
const fitDomain = this.#reactiveConfig.fitDomain;
|
|
500
|
-
|
|
501
|
-
if (!tooltipWanted)
|
|
720
|
+
if (tooltipConfig === false && !fitDomain)
|
|
502
721
|
return;
|
|
503
722
|
const dataVersion = this.dataVersion;
|
|
504
723
|
if (dataVersion === 0)
|
|
505
724
|
return;
|
|
725
|
+
const hovered = this.#chartContext.hovered ?? true;
|
|
726
|
+
const eager = !!fitDomain && this.#transformCompiler.hasJSTransforms();
|
|
506
727
|
untrack(() => {
|
|
728
|
+
if (dataVersion === this.#tooltipWrittenFor)
|
|
729
|
+
return;
|
|
507
730
|
clearTimeout(this.#tooltipWriteTimeout);
|
|
508
|
-
|
|
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 = () => {
|
|
509
741
|
const data = this.data;
|
|
510
|
-
if (data
|
|
511
|
-
|
|
512
|
-
|
|
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();
|
|
513
755
|
});
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
756
|
+
return () => clearTimeout(this.#tooltipWriteTimeout);
|
|
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);
|
|
520
789
|
});
|
|
521
790
|
// fitDomain — query DuckDB for fitted axis extent on driving axis change.
|
|
522
791
|
$effect(() => {
|
|
@@ -538,6 +807,16 @@ export class MarkLifecycleV2 {
|
|
|
538
807
|
const fittedSS = fittedAxis === 'x' ? this.#chartContext.xScaleState : this.#chartContext.yScaleState;
|
|
539
808
|
const fittedUserDriven = fittedSS?.userDriven ?? false;
|
|
540
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;
|
|
541
820
|
if (fittedSS) {
|
|
542
821
|
fittedSS.fitDomainActive = true;
|
|
543
822
|
}
|
|
@@ -548,6 +827,9 @@ export class MarkLifecycleV2 {
|
|
|
548
827
|
if (this.#fitDomainSuppressed && !drivingUserDriven) {
|
|
549
828
|
this.#fitDomainSuppressed = false;
|
|
550
829
|
}
|
|
830
|
+
// The driving axis moved: whatever fit follows is that zoom's, and
|
|
831
|
+
// springs.
|
|
832
|
+
this.#snapFitVersion = null;
|
|
551
833
|
if (this.#fitDomainSuppressed)
|
|
552
834
|
return;
|
|
553
835
|
clearTimeout(this.#fitDomainTimeout);
|
|
@@ -560,7 +842,11 @@ export class MarkLifecycleV2 {
|
|
|
560
842
|
// ===========================================================================
|
|
561
843
|
// PHASE 1: SETUP
|
|
562
844
|
// ===========================================================================
|
|
563
|
-
|
|
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) {
|
|
564
850
|
if (this.#destroyed)
|
|
565
851
|
return;
|
|
566
852
|
// Wait for the colour scale — and the series scale, which takes a dim the
|
|
@@ -596,9 +882,30 @@ export class MarkLifecycleV2 {
|
|
|
596
882
|
// a chart that was never going to get a colour scale.
|
|
597
883
|
if (!colorReady)
|
|
598
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) {
|
|
599
903
|
// Passive scales: skip domain fetching but still need schema for CAST decisions
|
|
600
904
|
if (this.#config.passiveScales) {
|
|
601
|
-
|
|
905
|
+
const columns = await this.#queryEngine.describe(this.#config.table);
|
|
906
|
+
if (superseded())
|
|
907
|
+
return;
|
|
908
|
+
this.#schema = buildSchemaMap(columns);
|
|
602
909
|
setScalesReady(this.#scaleMap, this.#channelsNeedingDataDomain, this.actualXScaleType, this.actualYScaleType, false, false);
|
|
603
910
|
this.setupComplete = true;
|
|
604
911
|
return;
|
|
@@ -612,12 +919,12 @@ export class MarkLifecycleV2 {
|
|
|
612
919
|
const [schemaColumns, fetchCount, aggResult, fieldStats] = await Promise.all([
|
|
613
920
|
// Fetch table schema (cached on QueryEngine, needed for CAST type decisions)
|
|
614
921
|
this.#queryEngine.describe(this.#config.table),
|
|
615
|
-
fetchCategoricalDomains(this.#queryEngine, this.#config.table, this.#config.select, this.actualXScaleType, this.actualYScaleType, this.#
|
|
616
|
-
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),
|
|
617
924
|
this.#fetchFieldStats(),
|
|
618
925
|
this.#ensureIdentityColumns()
|
|
619
926
|
]);
|
|
620
|
-
if (
|
|
927
|
+
if (superseded())
|
|
621
928
|
return;
|
|
622
929
|
this.#schema = buildSchemaMap(schemaColumns);
|
|
623
930
|
if (aggResult.globalAggregatesSet)
|
|
@@ -638,8 +945,15 @@ export class MarkLifecycleV2 {
|
|
|
638
945
|
if (error?.name === 'AbortError')
|
|
639
946
|
return;
|
|
640
947
|
console.error(`[MarkLifecycleV2] Setup failed for ${this.#markId}:`, error);
|
|
948
|
+
this.#releaseEntrance();
|
|
641
949
|
}
|
|
642
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
|
+
}
|
|
643
957
|
// ===========================================================================
|
|
644
958
|
// QUERY HELPERS
|
|
645
959
|
// ===========================================================================
|
|
@@ -735,13 +1049,10 @@ export class MarkLifecycleV2 {
|
|
|
735
1049
|
['x', ['x', 'x1', 'x2']],
|
|
736
1050
|
['y', ['y', 'y1', 'y2']]
|
|
737
1051
|
]) {
|
|
738
|
-
const
|
|
739
|
-
if (
|
|
1052
|
+
const t0Epoch = this.#timeOrigin(axis);
|
|
1053
|
+
if (t0Epoch === null)
|
|
740
1054
|
continue;
|
|
741
1055
|
const scaleState = this.#chartContext[`${axis}ScaleState`];
|
|
742
|
-
if (!scaleState?.t0)
|
|
743
|
-
continue;
|
|
744
|
-
const t0Epoch = Math.floor(scaleState.t0.getTime() / 1000);
|
|
745
1056
|
for (const ch of channels) {
|
|
746
1057
|
if (!(ch in select))
|
|
747
1058
|
continue;
|
|
@@ -757,6 +1068,20 @@ export class MarkLifecycleV2 {
|
|
|
757
1068
|
}
|
|
758
1069
|
}
|
|
759
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
|
+
}
|
|
760
1085
|
/**
|
|
761
1086
|
* Fetch MIN/MAX stats for continuous scale fields.
|
|
762
1087
|
* Replaces Mosaic's queryFieldInfo (old Effect 5 fieldInfo path).
|
|
@@ -825,7 +1150,8 @@ export class MarkLifecycleV2 {
|
|
|
825
1150
|
}
|
|
826
1151
|
}
|
|
827
1152
|
const q = Query.from(this.#config.table).select(selectDef);
|
|
828
|
-
|
|
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 });
|
|
829
1155
|
const row = result.get(0);
|
|
830
1156
|
if (!row)
|
|
831
1157
|
return null;
|
|
@@ -872,8 +1198,7 @@ export class MarkLifecycleV2 {
|
|
|
872
1198
|
const additionalChannels = this.#transformCompiler.getAdditionalChannels();
|
|
873
1199
|
const xState = this.#chartContext.xScaleState;
|
|
874
1200
|
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.#
|
|
876
|
-
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);
|
|
877
1202
|
this.#dataDerivedDomainsSet = domainResult.dataDerivedDomainsSet;
|
|
878
1203
|
this.#skipDomainUpdateForAxis = domainResult.skipDomainUpdateForAxis;
|
|
879
1204
|
markDeferredScalesReady(this.#scaleMap, this.#channelsNeedingDataDomain, this.actualXScaleType, this.actualYScaleType);
|
|
@@ -987,6 +1312,13 @@ export class MarkLifecycleV2 {
|
|
|
987
1312
|
// the view. Failing here gives a message naming the mark and the fields;
|
|
988
1313
|
// continuing would fail at the binder with neither.
|
|
989
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;
|
|
990
1322
|
for (const { key } of categorical)
|
|
991
1323
|
this.#identityFields.add(key);
|
|
992
1324
|
// Tell the transforms which dim each categorical channel is identified by.
|
|
@@ -1007,11 +1339,15 @@ export class MarkLifecycleV2 {
|
|
|
1007
1339
|
const identity = {};
|
|
1008
1340
|
const describe = (channel, key, positional) => {
|
|
1009
1341
|
const present = this.#queryEngine.presentTableFor(key, this.#config.table);
|
|
1342
|
+
// nulls="drop": its rows are filtered out, and dense() must not pad a
|
|
1343
|
+
// zero row for it either, or a sorted axis keeps a "(null)" slot.
|
|
1344
|
+
const nullId = this.#reactiveConfig.nulls === 'drop' ? this.#queryEngine.identityFor(key).nullId : null;
|
|
1010
1345
|
identity[channel] = {
|
|
1011
1346
|
dim: this.#queryEngine.dimTableFor(key),
|
|
1012
1347
|
size: this.#queryEngine.identityFor(key).values.length,
|
|
1013
1348
|
positional,
|
|
1014
|
-
...(present ? { present } : {})
|
|
1349
|
+
...(present ? { present } : {}),
|
|
1350
|
+
...(nullId !== null ? { dropNullId: nullId } : {})
|
|
1015
1351
|
};
|
|
1016
1352
|
};
|
|
1017
1353
|
for (const { axis, key } of axes)
|
|
@@ -1136,6 +1472,67 @@ export class MarkLifecycleV2 {
|
|
|
1136
1472
|
this.#releaseOther(scale);
|
|
1137
1473
|
}
|
|
1138
1474
|
}
|
|
1475
|
+
#warnedFollow = false;
|
|
1476
|
+
/**
|
|
1477
|
+
* Whether this mark follows its band axes instead of adding its categories to
|
|
1478
|
+
* them (`contributeDomain={false}`). It cannot also choose its own categories
|
|
1479
|
+
* or their order: a topN or a ranking would be what a follower exists not to
|
|
1480
|
+
* do, and an explicit domain is a contribution. Such a mark is refused, once,
|
|
1481
|
+
* and contributes as usual.
|
|
1482
|
+
*/
|
|
1483
|
+
get #followsDomain() {
|
|
1484
|
+
if (this.#reactiveConfig.contributeDomain !== false)
|
|
1485
|
+
return false;
|
|
1486
|
+
const reason = this.#transformCompiler.hasCategoryReducingTransforms()
|
|
1487
|
+
? 'a category-reducing transform (topN)'
|
|
1488
|
+
: this.#rankingSort
|
|
1489
|
+
? 'a sort'
|
|
1490
|
+
: this.#reactiveConfig.domain
|
|
1491
|
+
? 'an explicit domain'
|
|
1492
|
+
: null;
|
|
1493
|
+
if (!reason)
|
|
1494
|
+
return true;
|
|
1495
|
+
if (!this.#warnedFollow) {
|
|
1496
|
+
this.#warnedFollow = true;
|
|
1497
|
+
console.error(`[${this.#markId}] Unsupported: contributeDomain={false} with ${reason}. A mark that ` +
|
|
1498
|
+
`follows its band axis cannot choose its own categories or their order. It will ` +
|
|
1499
|
+
`contribute to the domain as usual.`);
|
|
1500
|
+
}
|
|
1501
|
+
return false;
|
|
1502
|
+
}
|
|
1503
|
+
/**
|
|
1504
|
+
* The band scale of an axis, for registering this mark's categories on it:
|
|
1505
|
+
* null for a mark that follows, so it registers nothing and withdraws what it
|
|
1506
|
+
* had (the prop can change). The scale still draws the mark's rows, only those
|
|
1507
|
+
* of categories it shows.
|
|
1508
|
+
*/
|
|
1509
|
+
#bandScaleForDomain(axis) {
|
|
1510
|
+
const scale = (axis === 'x' ? this.#chartContext.xScaleState : this.#chartContext.yScaleState);
|
|
1511
|
+
// Only a band or point axis has categories to follow; a value axis is
|
|
1512
|
+
// the mark's own.
|
|
1513
|
+
const type = axis === 'x' ? this.actualXScaleType : this.actualYScaleType;
|
|
1514
|
+
if ((type !== 'band' && type !== 'point') || !this.#followsDomain)
|
|
1515
|
+
return scale;
|
|
1516
|
+
scale?.removeMark(this.#markId);
|
|
1517
|
+
return null;
|
|
1518
|
+
}
|
|
1519
|
+
/** What this mark's Other rows hold, and what is done with it. */
|
|
1520
|
+
otherRows;
|
|
1521
|
+
/** What the mark is, for whether its Other bar splits; null before setup completes. */
|
|
1522
|
+
get #splitPlan() {
|
|
1523
|
+
if (!this.setupComplete)
|
|
1524
|
+
return null;
|
|
1525
|
+
const markType = this.#transformConfig.markType;
|
|
1526
|
+
const length = markType === 'barx' ? this.#transformConfig.x : this.#transformConfig.y;
|
|
1527
|
+
return {
|
|
1528
|
+
markType,
|
|
1529
|
+
length,
|
|
1530
|
+
by: this.#transformCompiler.otherBy(),
|
|
1531
|
+
expr: length ? this.#config.select[length] : undefined,
|
|
1532
|
+
hasOther: this.#transformCompiler.hasSyntheticOtherBucket(),
|
|
1533
|
+
hasJsTransforms: this.#transformCompiler.jsTransforms.length > 0
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1139
1536
|
/** The band scales whose Other bucket this mark holds a claim on. */
|
|
1140
1537
|
#otherScales = new Set();
|
|
1141
1538
|
#releaseOther(scale) {
|
|
@@ -1168,6 +1565,11 @@ export class MarkLifecycleV2 {
|
|
|
1168
1565
|
* never mixed, the other axis's channels morph, and the band scale owns the
|
|
1169
1566
|
* clock both run on.
|
|
1170
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
|
+
*
|
|
1171
1573
|
* The first channel of the transforms' alignment — `dense()`'s plan, over the
|
|
1172
1574
|
* same select and facts its `apply()` sees — so the conditions are written
|
|
1173
1575
|
* once. Derived rather than recorded at apply time: apply runs for every
|
|
@@ -1181,10 +1583,13 @@ export class MarkLifecycleV2 {
|
|
|
1181
1583
|
void this.#identity;
|
|
1182
1584
|
// Asked exactly as assembleQuery asks when it orders the query, so this and
|
|
1183
1585
|
// the rows cannot disagree about which channel they are aligned on.
|
|
1184
|
-
const
|
|
1185
|
-
groupKeys: parts.groupKeys
|
|
1586
|
+
const aligned = this.#transformCompiler.alignment(Object.keys(parts.selectDef), {
|
|
1587
|
+
groupKeys: parts.groupKeys,
|
|
1588
|
+
bins: parts.bins ?? null
|
|
1186
1589
|
})?.[0];
|
|
1187
|
-
|
|
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;
|
|
1188
1593
|
});
|
|
1189
1594
|
/**
|
|
1190
1595
|
* Build a query for fetchGlobalAggregates.
|
|
@@ -1195,10 +1600,12 @@ export class MarkLifecycleV2 {
|
|
|
1195
1600
|
// Build raw query parts directly from config.select (no identity columns,
|
|
1196
1601
|
// no bin resolution — scale states aren't ready yet during Phase 1).
|
|
1197
1602
|
const parts = buildQueryParts(this.#config.select, this.#config._orderConfig ?? null, this.#transformCompiler.hasSQLTransforms());
|
|
1603
|
+
// Domains from the rows the mark draws: without what nulls="drop" leaves
|
|
1604
|
+
// out. This select is raw, so the filter is on the raw values.
|
|
1198
1605
|
const q = assembleQuery({
|
|
1199
1606
|
table: this.#config.table,
|
|
1200
1607
|
parts,
|
|
1201
|
-
filter,
|
|
1608
|
+
filter: [...filter, ...this.#rawNullFilters(this.#config.select)],
|
|
1202
1609
|
transformCompiler: this.#transformCompiler
|
|
1203
1610
|
});
|
|
1204
1611
|
if (!q)
|
|
@@ -1257,9 +1664,10 @@ export class MarkLifecycleV2 {
|
|
|
1257
1664
|
// --- Selection routing ---
|
|
1258
1665
|
/**
|
|
1259
1666
|
* The identity keys of the categorical fields this mark's renderer reads
|
|
1260
|
-
* selection activity for, sorted: its ordinal colour
|
|
1261
|
-
* activity-reads.ts says the renderer reads
|
|
1262
|
-
* 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.
|
|
1263
1671
|
*/
|
|
1264
1672
|
#reads = $derived.by(() => {
|
|
1265
1673
|
const renderer = rendererForMark(this.#config.markType);
|
|
@@ -1273,10 +1681,15 @@ export class MarkLifecycleV2 {
|
|
|
1273
1681
|
const band = this.#chartContext[`${axis}ScaleState`];
|
|
1274
1682
|
return band?.identityKey ?? null;
|
|
1275
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');
|
|
1276
1688
|
return readsForResponse(this.#reactiveConfig.selectionResponse, readsFor(activityReads(renderer, this.#config.ruleExtent ?? false), {
|
|
1277
1689
|
color: color?.isOrdinal ? color.identityKey : null,
|
|
1278
1690
|
x: bandKey('x'),
|
|
1279
|
-
y: bandKey('y')
|
|
1691
|
+
y: bandKey('y'),
|
|
1692
|
+
series: ownSeries ? (this.#seriesScaleState?.identityKey ?? null) : null
|
|
1280
1693
|
}));
|
|
1281
1694
|
});
|
|
1282
1695
|
/** `#reads` as one string, so the registration effect re-runs only on a real change. */
|
|
@@ -1291,6 +1704,24 @@ export class MarkLifecycleV2 {
|
|
|
1291
1704
|
// NO_GROUP for selectionResponse 'filter', which never dims.
|
|
1292
1705
|
return groupIndexForResponse(this.#reactiveConfig.selectionResponse, this.#filterGroup.index);
|
|
1293
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);
|
|
1294
1725
|
get dataVersion() {
|
|
1295
1726
|
return this.#handle?.dataVersion ?? 0;
|
|
1296
1727
|
}
|
|
@@ -1436,8 +1867,14 @@ export class MarkLifecycleV2 {
|
|
|
1436
1867
|
// ===========================================================================
|
|
1437
1868
|
// TOOLTIP WRITE-BACK
|
|
1438
1869
|
// ===========================================================================
|
|
1439
|
-
|
|
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) {
|
|
1440
1875
|
const tableName = this.#tooltipTableName;
|
|
1876
|
+
// Before any statement: a write that fails part way can leave a table.
|
|
1877
|
+
this.#tooltipTableWritten = true;
|
|
1441
1878
|
try {
|
|
1442
1879
|
if (!this.#transformCompiler.hasJSTransforms()) {
|
|
1443
1880
|
// Path A: Build a query with original field names (not CAST-wrapped).
|
|
@@ -1480,11 +1917,17 @@ export class MarkLifecycleV2 {
|
|
|
1480
1917
|
replaceExprs.push(`CAST(${c} AS DOUBLE) AS ${c}`);
|
|
1481
1918
|
}
|
|
1482
1919
|
const replaceClause = replaceExprs.length > 0 ? ` REPLACE (${replaceExprs.join(', ')})` : '';
|
|
1483
|
-
//
|
|
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.
|
|
1484
1928
|
let orderClause = '';
|
|
1485
|
-
const tooltipSortTable = typeof this.#reactiveConfig.tooltip === 'object'
|
|
1486
|
-
|
|
1487
|
-
: true;
|
|
1929
|
+
const tooltipSortTable = typeof this.#reactiveConfig.tooltip === 'object' &&
|
|
1930
|
+
this.#reactiveConfig.tooltip.sortTable === true;
|
|
1488
1931
|
if (tooltipSortTable) {
|
|
1489
1932
|
const orderCol = ['x', 'x1', 'x2'].find((c) => c in selectConfig);
|
|
1490
1933
|
if (orderCol)
|
|
@@ -1496,10 +1939,8 @@ export class MarkLifecycleV2 {
|
|
|
1496
1939
|
// the rest are scalars (the rank, id columns), cheap to keep, and the
|
|
1497
1940
|
// lists are the only ones whose size grows with the field.
|
|
1498
1941
|
const withoutMembers = withoutOtherMembers(tooltipQuery);
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
`CREATE TEMP TABLE "${tableName}" AS SELECT *${replaceClause} FROM (${withoutMembers})${orderClause}`
|
|
1502
|
-
]);
|
|
1942
|
+
// One statement: a hover query never finds the table missing.
|
|
1943
|
+
await this.#queryEngine.exec(`CREATE OR REPLACE TEMP TABLE "${tableName}" AS SELECT *${replaceClause} FROM (${withoutMembers})${orderClause}`, { class: cls, connectionLocal: true });
|
|
1503
1944
|
}
|
|
1504
1945
|
else {
|
|
1505
1946
|
// Path B: Serialize transformed data via SQL VALUES.
|
|
@@ -1541,19 +1982,22 @@ export class MarkLifecycleV2 {
|
|
|
1541
1982
|
`DROP TABLE IF EXISTS "${tableName}"`,
|
|
1542
1983
|
`CREATE TEMP TABLE "${tableName}" (${schemaDef})`,
|
|
1543
1984
|
`INSERT INTO "${tableName}" VALUES ${valueRows.join(', ')}`
|
|
1544
|
-
]);
|
|
1985
|
+
], { class: cls, connectionLocal: true });
|
|
1545
1986
|
}
|
|
1546
1987
|
// Merge tip expression extras (aggregates/windows) via sidecar query.
|
|
1547
1988
|
const tipExtras = this.#getTipExtras();
|
|
1548
1989
|
if (Object.keys(tipExtras).length > 0) {
|
|
1549
1990
|
try {
|
|
1550
|
-
await this.#mergeTipExtras(tableName, tipExtras);
|
|
1991
|
+
await this.#mergeTipExtras(tableName, tipExtras, cls);
|
|
1551
1992
|
}
|
|
1552
1993
|
catch (err) {
|
|
1553
1994
|
console.warn('Failed to merge tip extras into tooltip table:', err);
|
|
1554
1995
|
}
|
|
1555
1996
|
}
|
|
1556
1997
|
this.#tooltipTableReady = true;
|
|
1998
|
+
// The row under a still pointer was read from the old table: read it
|
|
1999
|
+
// again, so the tooltip and a click show the new data without a move.
|
|
2000
|
+
this.#chartContext.tooltipManager?.refresh();
|
|
1557
2001
|
this.#onTooltipTableWritten();
|
|
1558
2002
|
}
|
|
1559
2003
|
catch (error) {
|
|
@@ -1607,7 +2051,7 @@ export class MarkLifecycleV2 {
|
|
|
1607
2051
|
* DB query. Computes aggregates against the source table using the same
|
|
1608
2052
|
* GROUP BY as the main query, then ALTER+UPDATE the temp table.
|
|
1609
2053
|
*/
|
|
1610
|
-
async #mergeTipExtras(tableName, tipExtras) {
|
|
2054
|
+
async #mergeTipExtras(tableName, tipExtras, cls) {
|
|
1611
2055
|
if (!this.#resolvedQueryParts)
|
|
1612
2056
|
return;
|
|
1613
2057
|
// Identify non-aggregate channel columns — these are the join keys
|
|
@@ -1624,6 +2068,9 @@ export class MarkLifecycleV2 {
|
|
|
1624
2068
|
}
|
|
1625
2069
|
Object.assign(sidecarSelect, tipExtras);
|
|
1626
2070
|
const sidecarParts = buildQueryParts(sidecarSelect, undefined, this.#transformCompiler.hasSQLTransforms());
|
|
2071
|
+
// The same rows as the mark: it leaves out what the mark leaves out. Its
|
|
2072
|
+
// select holds every non-aggregate channel, so each dropped one is there.
|
|
2073
|
+
sidecarParts.nullDrops = this.#resolvedQueryParts.nullDrops;
|
|
1627
2074
|
const filter = this.#filterGroup.selection.predicate(this.#route) ?? [];
|
|
1628
2075
|
const sidecarQuery = assembleQuery({
|
|
1629
2076
|
table: this.#sourceTable,
|
|
@@ -1642,7 +2089,10 @@ export class MarkLifecycleV2 {
|
|
|
1642
2089
|
.map((ch) => `${ident(tableName)}.${ident(ch)} = _s.${ident(ch)}`)
|
|
1643
2090
|
.join(' AND ');
|
|
1644
2091
|
const updateStmt = `UPDATE ${ident(tableName)} SET ${setClauses} FROM (${sidecarQuery.toString()}) _s WHERE ${joinCondition}`;
|
|
1645
|
-
await this.#queryEngine.exec([...alterStmts, updateStmt]
|
|
2092
|
+
await this.#queryEngine.exec([...alterStmts, updateStmt], {
|
|
2093
|
+
class: cls,
|
|
2094
|
+
connectionLocal: true
|
|
2095
|
+
});
|
|
1646
2096
|
}
|
|
1647
2097
|
// ===========================================================================
|
|
1648
2098
|
// FIT DOMAIN
|
|
@@ -1675,7 +2125,9 @@ export class MarkLifecycleV2 {
|
|
|
1675
2125
|
}
|
|
1676
2126
|
return channels;
|
|
1677
2127
|
}
|
|
1678
|
-
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) {
|
|
1679
2131
|
if (!force && drivingDomain) {
|
|
1680
2132
|
const [lastLo, lastHi] = this.#fitDomainLastTarget;
|
|
1681
2133
|
const lastWidth = lastHi - lastLo;
|
|
@@ -1694,7 +2146,11 @@ export class MarkLifecycleV2 {
|
|
|
1694
2146
|
return;
|
|
1695
2147
|
const result = await this.#queryEngine.query(q.toString(), {
|
|
1696
2148
|
callerId: `${this.#markId}:fitDomain`,
|
|
1697
|
-
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
|
|
1698
2154
|
});
|
|
1699
2155
|
if (seq !== this.#fitDomainQuerySequence)
|
|
1700
2156
|
return;
|
|
@@ -1719,11 +2175,14 @@ export class MarkLifecycleV2 {
|
|
|
1719
2175
|
}
|
|
1720
2176
|
const scaleState = fittedAxis === 'x' ? this.#chartContext.xScaleState : this.#chartContext.yScaleState;
|
|
1721
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);
|
|
1722
2181
|
if (scaleType === 'time' && scaleState && 'addOffsetExtent' in scaleState) {
|
|
1723
|
-
scaleState.addOffsetExtent([lo, hi], this.#markId);
|
|
2182
|
+
scaleState.addOffsetExtent([lo, hi], this.#markId, { instant });
|
|
1724
2183
|
}
|
|
1725
2184
|
else if (scaleState && 'addMark' in scaleState) {
|
|
1726
|
-
scaleState.addMark([lo, hi], this.#markId);
|
|
2185
|
+
scaleState.addMark([lo, hi], this.#markId, { instant });
|
|
1727
2186
|
}
|
|
1728
2187
|
if (drivingDomain)
|
|
1729
2188
|
this.#fitDomainLastTarget = [...drivingDomain];
|
|
@@ -1790,15 +2249,32 @@ export class MarkLifecycleV2 {
|
|
|
1790
2249
|
return q;
|
|
1791
2250
|
}
|
|
1792
2251
|
/**
|
|
1793
|
-
*
|
|
1794
|
-
*
|
|
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).
|
|
1795
2255
|
*/
|
|
1796
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() {
|
|
1797
2267
|
const fitDomain = this.#reactiveConfig.fitDomain;
|
|
1798
2268
|
if (!fitDomain)
|
|
1799
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;
|
|
1800
2274
|
if (this.#fitDomainSuppressed)
|
|
1801
2275
|
return;
|
|
2276
|
+
if (snap)
|
|
2277
|
+
this.#snapFitVersion = this.dataVersion;
|
|
1802
2278
|
const fittedAxis = this.#resolveFittedAxis(fitDomain);
|
|
1803
2279
|
if (!fittedAxis)
|
|
1804
2280
|
return;
|
|
@@ -1807,25 +2283,45 @@ export class MarkLifecycleV2 {
|
|
|
1807
2283
|
if (!drivingSS)
|
|
1808
2284
|
return;
|
|
1809
2285
|
if (drivingSS.scaleType === 'band') {
|
|
1810
|
-
this.#issueFitDomainQuery(fittedAxis, null, true);
|
|
2286
|
+
this.#issueFitDomainQuery(fittedAxis, null, true, snap);
|
|
1811
2287
|
return;
|
|
1812
2288
|
}
|
|
1813
2289
|
const target = drivingSS.domain?.target;
|
|
1814
2290
|
if (!target)
|
|
1815
2291
|
return;
|
|
1816
|
-
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();
|
|
1817
2308
|
}
|
|
1818
2309
|
// ===========================================================================
|
|
1819
2310
|
// CLEANUP
|
|
1820
2311
|
// ===========================================================================
|
|
1821
2312
|
destroy() {
|
|
1822
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).
|
|
1823
2317
|
this.#identityResetOff?.();
|
|
1824
2318
|
this.#identityResetOff = null;
|
|
2319
|
+
this.otherRows.unregister();
|
|
1825
2320
|
for (const scale of [...this.#otherScales])
|
|
1826
2321
|
this.#releaseOther(scale);
|
|
1827
2322
|
this.#handle?.destroy();
|
|
1828
2323
|
clearTimeout(this.#fitDomainTimeout);
|
|
2324
|
+
clearTimeout(this.#refitTimeout);
|
|
1829
2325
|
clearTimeout(this.#tooltipWriteTimeout);
|
|
1830
2326
|
if (this.#onMarkLOD) {
|
|
1831
2327
|
this.#chartContext.config.unregisterMarkLOD(this.#markId);
|
|
@@ -1841,6 +2337,23 @@ export class MarkLifecycleV2 {
|
|
|
1841
2337
|
// where destroy() is called synchronously before Svelte processes
|
|
1842
2338
|
// effect teardown.
|
|
1843
2339
|
this.#tooltipTableReady = false;
|
|
1844
|
-
this.#
|
|
2340
|
+
this.#dropTooltipTable();
|
|
1845
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(() => { });
|
|
2354
|
+
}
|
|
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]);
|
|
1846
2359
|
}
|