@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SelectQuery } from '@uwdata/mosaic-sql';
|
|
1
|
+
import type { ExprNode, SelectQuery } from '@uwdata/mosaic-sql';
|
|
2
2
|
import type { Transform, SqlTransform, JsTransform, MarkConfig, ScaleMap } from '../types';
|
|
3
3
|
import type { DataAccessor } from '../data/DataAccessor';
|
|
4
4
|
export declare class TransformCompiler {
|
|
@@ -19,7 +19,50 @@ export declare class TransformCompiler {
|
|
|
19
19
|
* than of the mark — its GROUP BY keys, say — merged over the mark config for
|
|
20
20
|
* this call only, so the shared config object is never written per query.
|
|
21
21
|
*/
|
|
22
|
-
applyToQuery(query: SelectQuery, facts?: Partial<MarkConfig
|
|
22
|
+
applyToQuery(query: SelectQuery, facts?: Partial<MarkConfig>,
|
|
23
|
+
/** The row filters are already in the table the query reads (a preagg table). */
|
|
24
|
+
options?: {
|
|
25
|
+
skipRowFilters?: boolean;
|
|
26
|
+
}): SelectQuery;
|
|
27
|
+
/**
|
|
28
|
+
* The padding rows of this mark's queries (see SqlTransform.padRows), for a
|
|
29
|
+
* pre-aggregated table to hold, or null when they cannot live in a table.
|
|
30
|
+
*
|
|
31
|
+
* Only when padding is the last thing that changes which rows exist: every
|
|
32
|
+
* other SQL transform is one that runs the same over a padded table as over
|
|
33
|
+
* a padded result. A topN ranks before padding, so zero rows in its table
|
|
34
|
+
* would enter the ranking; anything else not listed is not known to be safe.
|
|
35
|
+
*/
|
|
36
|
+
padRows(aliases: string[], facts?: Partial<MarkConfig>): string | null;
|
|
37
|
+
/**
|
|
38
|
+
* Whether a SQL transform filters on the groups' aggregates (`having`). Its
|
|
39
|
+
* condition names aggregates over the source rows, which a pre-aggregated table
|
|
40
|
+
* would recompute over its own groups: `count(*)` counts the table's groups, not
|
|
41
|
+
* the rows, so it gives a different answer with no error.
|
|
42
|
+
*
|
|
43
|
+
* Only the user-facing `having()` does this. The built-ins (topN, rank, stack,
|
|
44
|
+
* normalize, dense) add no HAVING: each wraps the grouped query in a subquery
|
|
45
|
+
* (`Query.from({ __topn_base: query })`) and adds window functions or joins, and
|
|
46
|
+
* those work the same on a preagg table's aggregated result, which is why a topN
|
|
47
|
+
* mark is served from preagg. The only CTE is assembleQuery's window wrapper for
|
|
48
|
+
* window aggregates, applied after the transforms. A test in transforms.test.ts
|
|
49
|
+
* fails if a built-in starts to emit a HAVING; this then has to know it.
|
|
50
|
+
*
|
|
51
|
+
* Supporting `having()` on a preagg table would mean rewriting its condition the
|
|
52
|
+
* way a select channel is rewritten (sufficient-statistics: collectAggregates,
|
|
53
|
+
* sufficientStats, rewrite; an aggregate not in the select adds a partial
|
|
54
|
+
* column) and applying it to the inner grouped query BEFORE the transforms wrap
|
|
55
|
+
* it, so it ends up inside their subquery as it does on the base table.
|
|
56
|
+
*/
|
|
57
|
+
hasHaving(): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* The row predicates of the SQL transforms (`filter`), for a pre-aggregated
|
|
60
|
+
* table to be built with: its read cannot apply them (see SqlTransform.where).
|
|
61
|
+
* A row filter applies before grouping, so building the table with it gives the
|
|
62
|
+
* same rows the base query sees; only `filter()` is one (see hasHaving for the
|
|
63
|
+
* transforms that are not).
|
|
64
|
+
*/
|
|
65
|
+
rowFilters(): ExprNode[];
|
|
23
66
|
/**
|
|
24
67
|
* Which channels a query with these aliases comes back aligned on, leading
|
|
25
68
|
* term first, or null. Asked with the same facts as `applyToQuery`, so the
|
|
@@ -60,5 +103,7 @@ export declare class TransformCompiler {
|
|
|
60
103
|
* before the transform can write it.
|
|
61
104
|
*/
|
|
62
105
|
hasSyntheticOtherBucket(): boolean;
|
|
106
|
+
/** The `by` a topN Other bucket ranks by when stated explicitly, else null (its default). */
|
|
107
|
+
otherBy(): string | null;
|
|
63
108
|
getCategoryTransformKey(): string | null;
|
|
64
109
|
}
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL transforms that give the same rows over a table holding its padding as
|
|
3
|
+
* padding gives their result: row filters (applied to the table's build), and
|
|
4
|
+
* what runs after padding (rank, stack, normalize).
|
|
5
|
+
*/
|
|
6
|
+
const PAD_SAFE = new Set(['dense', 'filter', 'rank', 'stack', 'normalize']);
|
|
1
7
|
export class TransformCompiler {
|
|
2
8
|
transforms;
|
|
3
9
|
sqlTransforms;
|
|
@@ -35,10 +41,14 @@ export class TransformCompiler {
|
|
|
35
41
|
* than of the mark — its GROUP BY keys, say — merged over the mark config for
|
|
36
42
|
* this call only, so the shared config object is never written per query.
|
|
37
43
|
*/
|
|
38
|
-
applyToQuery(query, facts
|
|
44
|
+
applyToQuery(query, facts,
|
|
45
|
+
/** The row filters are already in the table the query reads (a preagg table). */
|
|
46
|
+
options) {
|
|
39
47
|
let result = query;
|
|
40
48
|
const config = this.#configFor(facts);
|
|
41
49
|
for (const transform of this.sqlTransforms) {
|
|
50
|
+
if (options?.skipRowFilters && transform.where !== undefined)
|
|
51
|
+
continue;
|
|
42
52
|
try {
|
|
43
53
|
result = transform.apply(result, config);
|
|
44
54
|
}
|
|
@@ -48,6 +58,59 @@ export class TransformCompiler {
|
|
|
48
58
|
}
|
|
49
59
|
return result;
|
|
50
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* The padding rows of this mark's queries (see SqlTransform.padRows), for a
|
|
63
|
+
* pre-aggregated table to hold, or null when they cannot live in a table.
|
|
64
|
+
*
|
|
65
|
+
* Only when padding is the last thing that changes which rows exist: every
|
|
66
|
+
* other SQL transform is one that runs the same over a padded table as over
|
|
67
|
+
* a padded result. A topN ranks before padding, so zero rows in its table
|
|
68
|
+
* would enter the ranking; anything else not listed is not known to be safe.
|
|
69
|
+
*/
|
|
70
|
+
padRows(aliases, facts) {
|
|
71
|
+
if (!this.sqlTransforms.every((t) => PAD_SAFE.has(t.type)))
|
|
72
|
+
return null;
|
|
73
|
+
const config = this.#configFor(facts);
|
|
74
|
+
for (const transform of this.sqlTransforms) {
|
|
75
|
+
const rows = transform.padRows?.(aliases, config);
|
|
76
|
+
if (rows)
|
|
77
|
+
return rows;
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Whether a SQL transform filters on the groups' aggregates (`having`). Its
|
|
83
|
+
* condition names aggregates over the source rows, which a pre-aggregated table
|
|
84
|
+
* would recompute over its own groups: `count(*)` counts the table's groups, not
|
|
85
|
+
* the rows, so it gives a different answer with no error.
|
|
86
|
+
*
|
|
87
|
+
* Only the user-facing `having()` does this. The built-ins (topN, rank, stack,
|
|
88
|
+
* normalize, dense) add no HAVING: each wraps the grouped query in a subquery
|
|
89
|
+
* (`Query.from({ __topn_base: query })`) and adds window functions or joins, and
|
|
90
|
+
* those work the same on a preagg table's aggregated result, which is why a topN
|
|
91
|
+
* mark is served from preagg. The only CTE is assembleQuery's window wrapper for
|
|
92
|
+
* window aggregates, applied after the transforms. A test in transforms.test.ts
|
|
93
|
+
* fails if a built-in starts to emit a HAVING; this then has to know it.
|
|
94
|
+
*
|
|
95
|
+
* Supporting `having()` on a preagg table would mean rewriting its condition the
|
|
96
|
+
* way a select channel is rewritten (sufficient-statistics: collectAggregates,
|
|
97
|
+
* sufficientStats, rewrite; an aggregate not in the select adds a partial
|
|
98
|
+
* column) and applying it to the inner grouped query BEFORE the transforms wrap
|
|
99
|
+
* it, so it ends up inside their subquery as it does on the base table.
|
|
100
|
+
*/
|
|
101
|
+
hasHaving() {
|
|
102
|
+
return this.sqlTransforms.some((t) => t.type === 'having');
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The row predicates of the SQL transforms (`filter`), for a pre-aggregated
|
|
106
|
+
* table to be built with: its read cannot apply them (see SqlTransform.where).
|
|
107
|
+
* A row filter applies before grouping, so building the table with it gives the
|
|
108
|
+
* same rows the base query sees; only `filter()` is one (see hasHaving for the
|
|
109
|
+
* transforms that are not).
|
|
110
|
+
*/
|
|
111
|
+
rowFilters() {
|
|
112
|
+
return this.sqlTransforms.flatMap((t) => (t.where === undefined ? [] : [t.where]));
|
|
113
|
+
}
|
|
51
114
|
/**
|
|
52
115
|
* Which channels a query with these aliases comes back aligned on, leading
|
|
53
116
|
* term first, or null. Asked with the same facts as `applyToQuery`, so the
|
|
@@ -147,6 +210,12 @@ export class TransformCompiler {
|
|
|
147
210
|
hasSyntheticOtherBucket() {
|
|
148
211
|
return this.transforms.some((t) => t.type === 'topN' && t.spec?.includeOthers === true);
|
|
149
212
|
}
|
|
213
|
+
/** The `by` a topN Other bucket ranks by when stated explicitly, else null (its default). */
|
|
214
|
+
otherBy() {
|
|
215
|
+
const t = this.transforms.find((t) => t.type === 'topN' && t.spec?.includeOthers === true);
|
|
216
|
+
const by = t?.spec?.by;
|
|
217
|
+
return typeof by === 'string' ? by : null;
|
|
218
|
+
}
|
|
150
219
|
getCategoryTransformKey() {
|
|
151
220
|
const parts = [];
|
|
152
221
|
for (const t of this.transforms) {
|
package/dist/transforms/index.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* - JS: Process data via the DataAccessor interface (zero-copy on Arrow data)
|
|
7
7
|
*/
|
|
8
8
|
import { Query, column, sql as sqlTag } from '@uwdata/mosaic-sql';
|
|
9
|
+
import { numericBinEdgeSQL } from '../sql/bin';
|
|
9
10
|
// =============================================================================
|
|
10
11
|
// SQL TRANSFORMS (Query Modifiers)
|
|
11
12
|
// =============================================================================
|
|
@@ -14,6 +15,7 @@ export function filter(condition) {
|
|
|
14
15
|
stage: 'sql',
|
|
15
16
|
type: 'filter',
|
|
16
17
|
spec: { transform: 'filter', condition: condition.toString() },
|
|
18
|
+
where: condition,
|
|
17
19
|
apply(query) {
|
|
18
20
|
return query.where(condition);
|
|
19
21
|
},
|
|
@@ -207,18 +209,29 @@ const BAND_SOURCE_COLUMN = '__dplot_band_src';
|
|
|
207
209
|
* instead, and the query assembler orders the outermost query by it.
|
|
208
210
|
*/
|
|
209
211
|
function densify(inner, aliases, plan) {
|
|
210
|
-
const { band, dim, cross } = plan;
|
|
212
|
+
const { band, dim, cross, bins } = plan;
|
|
211
213
|
const crossAlias = new Map(cross.map(([key], i) => [key, `k${i}`]));
|
|
214
|
+
const binOf = new Map(bins);
|
|
212
215
|
const select = {};
|
|
213
216
|
for (const alias of aliases) {
|
|
214
217
|
const crossed = crossAlias.get(alias);
|
|
215
|
-
|
|
216
|
-
|
|
218
|
+
const bin = binOf.get(alias);
|
|
219
|
+
if (bin) {
|
|
220
|
+
// Every row's edge from its bin index, real and padded alike, so the
|
|
221
|
+
// edges are the same bits on every update: they are what tells the
|
|
222
|
+
// renderer the row set has not changed (alignmentKey).
|
|
223
|
+
select[alias] = rawSql(binEdge(bin, 'd.k'));
|
|
224
|
+
}
|
|
225
|
+
else if (alias === band) {
|
|
226
|
+
// REAL, as the inner query's id columns are: an INTEGER column would
|
|
227
|
+
// reach the GPU through a copy, and hand the renderer other bits than
|
|
228
|
+
// a preagg read, which keeps the REAL ids.
|
|
229
|
+
select[alias] = rawSql(`CAST(d."id" AS REAL)`);
|
|
217
230
|
}
|
|
218
231
|
else if (crossed) {
|
|
219
232
|
// Another group key, enumerated from its own dim: the padded row for a
|
|
220
233
|
// missing (category, series) pair carries that series' real id.
|
|
221
|
-
select[alias] = rawSql(
|
|
234
|
+
select[alias] = rawSql(`CAST(${crossed}."id" AS REAL)`);
|
|
222
235
|
}
|
|
223
236
|
else if (alias.startsWith(OTHER_MEMBER_PREFIX)) {
|
|
224
237
|
// Other's member lists: NULL on a padded row, as on every row but
|
|
@@ -226,7 +239,7 @@ function densify(inner, aliases, plan) {
|
|
|
226
239
|
// a list.
|
|
227
240
|
select[alias] = rawSql(`a.${column(alias)}`);
|
|
228
241
|
}
|
|
229
|
-
else if (plan.identity[alias]?.dim === dim) {
|
|
242
|
+
else if (!bins && plan.identity[alias]?.dim === dim) {
|
|
230
243
|
// Same dim as the band, so this channel IS the category — the ordinary
|
|
231
244
|
// "colour the bars by their own category" chart. Its value for a row
|
|
232
245
|
// with no data is not unknown, it is that category's id, so take it
|
|
@@ -235,7 +248,7 @@ function densify(inner, aliases, plan) {
|
|
|
235
248
|
// This is what keeps a DEPARTING bar its colour. It is still on screen
|
|
236
249
|
// while it shrinks, and a sentinel would turn it grey on the frame its
|
|
237
250
|
// data disappeared — the one frame where the colour matters most.
|
|
238
|
-
select[alias] =
|
|
251
|
+
select[alias] = rawSql(`CAST(d."id" AS REAL)`);
|
|
239
252
|
}
|
|
240
253
|
else if (plan.identity[alias]) {
|
|
241
254
|
// A category id from some OTHER field that is not a group key — an
|
|
@@ -243,7 +256,7 @@ function densify(inner, aliases, plan) {
|
|
|
243
256
|
// so the absent sentinel it is. Never 0, which is a real category: it
|
|
244
257
|
// would enter the colour scale's domain, shift every palette rank after
|
|
245
258
|
// it, and repaint bars that had not changed.
|
|
246
|
-
select[alias] = sqlTag `COALESCE(a.${column(alias)}, -1)`;
|
|
259
|
+
select[alias] = sqlTag `COALESCE(a.${column(alias)}, CAST(-1 AS REAL))`;
|
|
247
260
|
}
|
|
248
261
|
else {
|
|
249
262
|
// A measure. 0 is the point: it is what makes a departing bar shrink to
|
|
@@ -274,11 +287,52 @@ function densify(inner, aliases, plan) {
|
|
|
274
287
|
const joins = cross
|
|
275
288
|
.map(([key, keyDim], i) => ` CROSS JOIN (SELECT "id" FROM ${padIds(plan.identity[key], keyDim)} UNION ALL SELECT -1) AS k${i}`)
|
|
276
289
|
.join('');
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
return Query.from(rawSql(`${
|
|
290
|
+
const bandBin = binOf.get(band);
|
|
291
|
+
const bandOn = bandBin ? binMatch(bandBin, `a.${column(band)}`) : `a.${column(band)} = d."id"`;
|
|
292
|
+
const on = [bandOn, ...cross.map(([key], i) => `a.${column(key)} = k${i}."id"`)].join(' AND ');
|
|
293
|
+
const source = bandBin ? binSource(bandBin) : `${padIds(plan.identity[band], dim)} AS d`;
|
|
294
|
+
return Query.from(rawSql(`${source}${joins} LEFT JOIN (${inner}) AS a ON ${on}`)).select(select);
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Every bin, as `d.k`: an index for numeric bins, a bucket start for time
|
|
298
|
+
* buckets. Time buckets come from `time_bucket` itself over the field's
|
|
299
|
+
* extent, so they are exactly the buckets the query's own expression makes,
|
|
300
|
+
* months and years included, which are not a fixed width.
|
|
301
|
+
*/
|
|
302
|
+
function binSource(bin) {
|
|
303
|
+
if (bin.kind === 'number')
|
|
304
|
+
return `range(0, ${bin.count}) AS d(k)`;
|
|
305
|
+
const first = `time_bucket(${bin.interval}, epoch_ms(${Math.round(bin.minMs)}))`;
|
|
306
|
+
const last = `time_bucket(${bin.interval}, epoch_ms(${Math.round(bin.maxMs)}))`;
|
|
307
|
+
return `range(${first}, ${last} + ${bin.interval}, ${bin.interval}) AS d(k)`;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Bin `k`'s edge as the REAL the query draws. Numeric: `min + alpha * (k +
|
|
311
|
+
* offset)`, in DOUBLE arithmetic. Time: the bucket start plus `offset`
|
|
312
|
+
* intervals, as seconds from `t0`, the form the mark's time channels take.
|
|
313
|
+
*/
|
|
314
|
+
function binEdge(bin, k) {
|
|
315
|
+
if (bin.kind === 'number') {
|
|
316
|
+
return `CAST(${numericBinEdgeSQL(bin.min, bin.alpha, bin.offset, k)} AS REAL)`;
|
|
317
|
+
}
|
|
318
|
+
return `CAST((epoch(${k} + (${bin.interval}) * ${bin.offset}) - ${bin.t0}) AS REAL)`;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Whether the result row's edge `edge` is bin `d.k`. Numeric bins match by
|
|
322
|
+
* index, recovered from the edge: the edge is a REAL in the result (and in a
|
|
323
|
+
* preagg table), so comparing it to an edge computed here could miss by a
|
|
324
|
+
* rounding. A time edge is computed by the same expression on both sides, so
|
|
325
|
+
* the REALs are equal.
|
|
326
|
+
*/
|
|
327
|
+
function binMatch(bin, edge) {
|
|
328
|
+
if (bin.kind === 'number') {
|
|
329
|
+
return `round((CAST(${edge} AS DOUBLE) - ${sqlNumber(bin.min)}) / ${sqlNumber(bin.alpha)}) - ${bin.offset} = d.k`;
|
|
330
|
+
}
|
|
331
|
+
return `${edge} = ${binEdge(bin, 'd.k')}`;
|
|
332
|
+
}
|
|
333
|
+
/** A JS number as a DOUBLE literal: a bare 0.1 would be read as a DECIMAL. */
|
|
334
|
+
function sqlNumber(n) {
|
|
335
|
+
return `CAST(${JSON.stringify(n)} AS DOUBLE)`;
|
|
282
336
|
}
|
|
283
337
|
/**
|
|
284
338
|
* Most padded rows `dense()` will produce before refusing.
|
|
@@ -303,9 +357,11 @@ export const DENSE_ROW_LIMIT = 10_000;
|
|
|
303
357
|
*/
|
|
304
358
|
function padIds(id, dim) {
|
|
305
359
|
const present = id?.present;
|
|
360
|
+
// A mark that drops NULL pads no row for it: its rows were filtered out.
|
|
361
|
+
const notNull = id?.dropNullId != null ? ` WHERE "id" <> ${id.dropNullId}` : '';
|
|
306
362
|
if (!present)
|
|
307
|
-
return `(SELECT "id" FROM ${quoteTable(dim)})`;
|
|
308
|
-
return (`(SELECT "id" FROM ${quoteTable(present)} UNION ALL ` +
|
|
363
|
+
return `(SELECT "id" FROM ${quoteTable(dim)}${notNull})`;
|
|
364
|
+
return (`(SELECT "id" FROM ${quoteTable(present)}${notNull} UNION ALL ` +
|
|
309
365
|
`SELECT "id" FROM ${quoteTable(dim)} WHERE "label" IS NOT NULL)`);
|
|
310
366
|
}
|
|
311
367
|
/**
|
|
@@ -344,20 +400,24 @@ function densePlan(aliases, config, enabled, reported) {
|
|
|
344
400
|
if (reported.has(key))
|
|
345
401
|
return;
|
|
346
402
|
reported.add(key);
|
|
347
|
-
console.info(`[dense] ${reason} Categories on this mark will snap between updates ` +
|
|
348
|
-
`rather than morphing, and
|
|
403
|
+
console.info(`[dense] ${reason} Categories or bins on this mark will snap between updates ` +
|
|
404
|
+
`rather than morphing, and one leaving will vanish rather than shrink out.`);
|
|
349
405
|
};
|
|
350
406
|
if (!enabled)
|
|
351
407
|
return null;
|
|
352
|
-
const identity = config?.identity;
|
|
353
|
-
if (!identity)
|
|
354
|
-
return null;
|
|
355
408
|
// Stated by whoever built the select, because it cannot be recovered from
|
|
356
409
|
// the query by the time this runs: topN has already wrapped it, and the
|
|
357
410
|
// GROUP BY is several levels down.
|
|
358
411
|
const groupKeys = config?.groupKeys;
|
|
359
412
|
if (!groupKeys)
|
|
360
413
|
return null;
|
|
414
|
+
// A histogram: its bins are enumerable like a dim's ids.
|
|
415
|
+
const binned = binPlan(aliases, groupKeys, config, say);
|
|
416
|
+
if (binned !== undefined)
|
|
417
|
+
return binned;
|
|
418
|
+
const identity = config?.identity;
|
|
419
|
+
if (!identity)
|
|
420
|
+
return null;
|
|
361
421
|
const candidates = aliases.filter((ch) => identity[ch]?.positional);
|
|
362
422
|
if (candidates.length !== 1) {
|
|
363
423
|
if (candidates.length > 1) {
|
|
@@ -397,7 +457,59 @@ function densePlan(aliases, config, enabled, reported) {
|
|
|
397
457
|
`(${size} categories${cross.length > 0 ? ` across ${cross.length + 1} group keys` : ''}).`);
|
|
398
458
|
return null;
|
|
399
459
|
}
|
|
400
|
-
return { band, dim, cross, identity };
|
|
460
|
+
return { band, dim, cross, bins: null, identity };
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* The plan for a binned axis: one row per bin, from the bins `bin()`
|
|
464
|
+
* resolved, crossed with each other group key's ids as for a categorical band.
|
|
465
|
+
* Null with nothing binned. Refuses, saying why, what it cannot enumerate: bins
|
|
466
|
+
* on both axes (a 2D histogram) or beside a categorical axis, a group key with
|
|
467
|
+
* no dim, and too many rows. Silent while identity has not landed for a group
|
|
468
|
+
* key that will get it.
|
|
469
|
+
*/
|
|
470
|
+
function binPlan(aliases, groupKeys, config, say) {
|
|
471
|
+
const specs = config?.bins;
|
|
472
|
+
if (!specs)
|
|
473
|
+
return undefined;
|
|
474
|
+
const bins = aliases
|
|
475
|
+
.filter((ch) => specs[ch] && groupKeys.includes(ch))
|
|
476
|
+
.map((ch) => [ch, specs[ch]])
|
|
477
|
+
.sort((a, b) => a[1].offset - b[1].offset);
|
|
478
|
+
if (bins.length === 0)
|
|
479
|
+
return undefined;
|
|
480
|
+
const identity = config?.identity ?? null;
|
|
481
|
+
if (new Set(bins.map(([ch]) => ch[0])).size > 1) {
|
|
482
|
+
say('Bins on both axes (a 2D histogram), and padding is defined for one.');
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
const categorical = identity ? aliases.filter((ch) => identity[ch]?.positional) : [];
|
|
486
|
+
if (categorical.length > 0) {
|
|
487
|
+
say(`A binned axis beside a categorical one (${categorical.join(', ')}).`);
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
const binned = new Set(bins.map(([ch]) => ch));
|
|
491
|
+
const cross = [];
|
|
492
|
+
let rows = bins[0][1].count;
|
|
493
|
+
for (const key of groupKeys) {
|
|
494
|
+
if (binned.has(key) || !aliases.includes(key))
|
|
495
|
+
continue;
|
|
496
|
+
const keyIdentity = identity?.[key];
|
|
497
|
+
// Before identity setup a series has no dim yet; it will.
|
|
498
|
+
if (!identity)
|
|
499
|
+
return null;
|
|
500
|
+
if (!keyIdentity) {
|
|
501
|
+
say(`the group key "${key}" has no dim, so its missing combinations cannot be enumerated.`);
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
cross.push([key, keyIdentity.dim]);
|
|
505
|
+
rows *= keyIdentity.size + 1;
|
|
506
|
+
}
|
|
507
|
+
if (rows > DENSE_ROW_LIMIT) {
|
|
508
|
+
say(`padding would produce about ${rows} rows, over the ${DENSE_ROW_LIMIT} limit ` +
|
|
509
|
+
`(${bins[0][1].count} bins${cross.length > 0 ? ` across ${cross.length + 1} group keys` : ''}).`);
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
return { band: bins[0][0], dim: '', cross, bins, identity: identity ?? {} };
|
|
401
513
|
}
|
|
402
514
|
/**
|
|
403
515
|
* Return a row for every category, not only the ones that survived.
|
|
@@ -437,10 +549,27 @@ export function dense(options = {}) {
|
|
|
437
549
|
return plan ? [plan.band, ...plan.cross.map(([key]) => key)] : null;
|
|
438
550
|
},
|
|
439
551
|
apply(query, config) {
|
|
552
|
+
// A preagg table built with padRows() below: its rows are already one
|
|
553
|
+
// per category or bin, and only the order is left, which the query's
|
|
554
|
+
// ORDER BY takes from alignment() as for any other read.
|
|
555
|
+
if (config?.padded)
|
|
556
|
+
return query;
|
|
440
557
|
const aliases = getSelectAliases(query);
|
|
441
558
|
const plan = densePlan(aliases, config, enabled, reported);
|
|
442
559
|
return plan ? densify(query, aliases, plan) : query;
|
|
443
560
|
},
|
|
561
|
+
/**
|
|
562
|
+
* The rows apply() would add, as the padding of an empty result: the same
|
|
563
|
+
* join and the same rules for every channel, so a table built with them
|
|
564
|
+
* holds exactly what padding its reads would have added.
|
|
565
|
+
*/
|
|
566
|
+
padRows(aliases, config) {
|
|
567
|
+
const plan = densePlan(aliases, config, enabled, reported);
|
|
568
|
+
if (!plan)
|
|
569
|
+
return null;
|
|
570
|
+
const empty = Query.select(Object.fromEntries(aliases.map((alias) => [alias, rawSql('NULL')]))).where(rawSql('false'));
|
|
571
|
+
return String(densify(empty, aliases, plan));
|
|
572
|
+
},
|
|
444
573
|
describe() {
|
|
445
574
|
return 'Dense categories';
|
|
446
575
|
}
|
|
@@ -806,7 +935,9 @@ export function stackX(options = {}) {
|
|
|
806
935
|
return stackSQL('x', options);
|
|
807
936
|
}
|
|
808
937
|
/** Resolve band (group) / value column aliases for a stack direction, validated against the select list. */
|
|
809
|
-
function resolveStackColumns(aliases, config, direction
|
|
938
|
+
function resolveStackColumns(aliases, config, direction,
|
|
939
|
+
/** Called when there is no series column; the caller decides whether that is worth saying. */
|
|
940
|
+
onNoSeries) {
|
|
810
941
|
const bandAxis = direction === 'y' ? 'x' : 'y';
|
|
811
942
|
const band = config?.[bandAxis];
|
|
812
943
|
const value = config?.[direction];
|
|
@@ -820,20 +951,31 @@ function resolveStackColumns(aliases, config, direction) {
|
|
|
820
951
|
}
|
|
821
952
|
const fill = config?.fill;
|
|
822
953
|
const series = typeof fill === 'string' && aliases.includes(fill) ? fill : null;
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
// identical to an unstacked mark. That is the correct result, but it is
|
|
826
|
-
// indistinguishable from a broken stack to anyone looking at the chart, so say
|
|
827
|
-
// what is missing rather than silently drawing an ordinary bar.
|
|
828
|
-
if (!series) {
|
|
829
|
-
console.warn(`stack${direction === 'y' ? 'Y' : 'X'}: no series column, so stacking has no effect ` +
|
|
830
|
-
`(one row per band). Stacking groups rows within a band by the ordinal ` +
|
|
831
|
-
`fill channel — add fill="<field>" to the mark. Select list: [${aliases.join(', ')}].`);
|
|
832
|
-
}
|
|
954
|
+
if (!series)
|
|
955
|
+
onNoSeries(aliases);
|
|
833
956
|
return { band, value, series };
|
|
834
957
|
}
|
|
835
|
-
|
|
958
|
+
/**
|
|
959
|
+
* `implicit`: added by the mark (withImplicitStack), not written by the user.
|
|
960
|
+
*
|
|
961
|
+
* With no series column there is one row per band, so the cumulative sum equals
|
|
962
|
+
* that row's own value and the stack is arithmetically a no-op. For a stack the
|
|
963
|
+
* user wrote, that is indistinguishable from a broken one on screen, so it says
|
|
964
|
+
* what is missing, once per transform rather than on every query assembled. An
|
|
965
|
+
* implicit stack says nothing: every bar mark without a fill gets one, and it is
|
|
966
|
+
* the ordinary bar it draws, not a mistake.
|
|
967
|
+
*/
|
|
968
|
+
function stackSQL(direction, options, implicit = false) {
|
|
836
969
|
const [loName, hiName] = direction === 'y' ? ['y0', 'y1'] : ['x0', 'x1'];
|
|
970
|
+
let warned = implicit;
|
|
971
|
+
const onNoSeries = (aliases) => {
|
|
972
|
+
if (warned)
|
|
973
|
+
return;
|
|
974
|
+
warned = true;
|
|
975
|
+
console.warn(`stack${direction === 'y' ? 'Y' : 'X'}: no series column, so stacking has no effect ` +
|
|
976
|
+
`(one row per band). Stacking groups rows within a band by the ordinal ` +
|
|
977
|
+
`fill channel — add fill="<field>" to the mark. Select list: [${aliases.join(', ')}].`);
|
|
978
|
+
};
|
|
837
979
|
return {
|
|
838
980
|
stage: 'sql',
|
|
839
981
|
type: 'stack',
|
|
@@ -844,7 +986,7 @@ function stackSQL(direction, options) {
|
|
|
844
986
|
channels: [loName, hiName],
|
|
845
987
|
apply(query, config) {
|
|
846
988
|
const aliases = getSelectAliases(query);
|
|
847
|
-
const { band, value, series } = resolveStackColumns(aliases, config, direction);
|
|
989
|
+
const { band, value, series } = resolveStackColumns(aliases, config, direction, onNoSeries);
|
|
848
990
|
const valueCol = column(value);
|
|
849
991
|
const bandCol = column(band);
|
|
850
992
|
const notNaN = sqlTag `NOT isnan(${valueCol})`;
|
|
@@ -954,12 +1096,6 @@ function stackXJS(options) {
|
|
|
954
1096
|
}
|
|
955
1097
|
};
|
|
956
1098
|
}
|
|
957
|
-
/**
|
|
958
|
-
* Core stacking computation on DataAccessor columns.
|
|
959
|
-
*
|
|
960
|
-
* Single O(n) pass: detects group boundaries in the sorted group column,
|
|
961
|
-
* accumulates value offsets, writes lo/hi Float32Arrays, applies offset mode.
|
|
962
|
-
*/
|
|
963
1099
|
/**
|
|
964
1100
|
* Core stacking computation on DataAccessor columns.
|
|
965
1101
|
*
|
|
@@ -1185,9 +1321,7 @@ export function withImplicitStack(transforms, hasExplicitBounds, direction) {
|
|
|
1185
1321
|
? direction === 'y'
|
|
1186
1322
|
? stackY({ engine: 'js' })
|
|
1187
1323
|
: stackX({ engine: 'js' })
|
|
1188
|
-
: direction
|
|
1189
|
-
? stackY()
|
|
1190
|
-
: stackX();
|
|
1324
|
+
: stackSQL(direction, {}, true);
|
|
1191
1325
|
return [...transforms, implicitStack];
|
|
1192
1326
|
}
|
|
1193
1327
|
export { TransformCompiler } from './TransformCompiler';
|