@fjader/svelte 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/components/axes/AxisBottom.svelte +23 -6
- package/dist/components/axes/AxisBottom.svelte.d.ts +10 -0
- package/dist/components/axes/AxisLeft.svelte +77 -8
- package/dist/components/axes/AxisLeft.svelte.d.ts +17 -0
- package/dist/components/axes/BandAxisTicks.svelte +1 -1
- package/dist/components/axes/axis-title.d.ts +30 -0
- package/dist/components/axes/axis-title.js +77 -0
- package/dist/components/charts/ChartCell.svelte +1 -1
- package/dist/components/charts/ChartContainer.svelte +10 -2
- package/dist/components/charts/ChartContainer.svelte.d.ts +7 -0
- package/dist/components/guides/legend/ContinuousColorLegend.svelte +7 -1
- package/dist/components/guides/legend/OrdinalColorLegend.svelte +8 -5
- package/dist/components/marks/AreaMarkX.svelte +2 -0
- package/dist/components/marks/AreaMarkY.svelte +2 -0
- package/dist/components/marks/BarMarkX.svelte +13 -1
- package/dist/components/marks/BarMarkY.svelte +13 -1
- package/dist/components/marks/DotsMark.svelte +2 -0
- package/dist/components/marks/LineMark.svelte +5 -1
- package/dist/components/marks/ParallelCoordinatesMark.svelte +4 -0
- package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +2 -0
- package/dist/components/marks/RectMark.svelte +2 -0
- package/dist/components/marks/RectXMark.svelte +2 -0
- package/dist/components/marks/RectYMark.svelte +2 -0
- package/dist/components/marks/RuleMarkX.svelte +2 -0
- package/dist/components/marks/RuleMarkY.svelte +2 -0
- package/dist/components/marks/TextMark.svelte +8 -9
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +6 -1
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +187 -18
- package/dist/components/marks/lifecycle/domain-queries.d.ts +3 -1
- package/dist/components/marks/lifecycle/domain-queries.js +11 -6
- package/dist/components/marks/lifecycle/domain-registration.js +2 -6
- package/dist/components/marks/lifecycle/other-rows.svelte.d.ts +104 -0
- package/dist/components/marks/lifecycle/other-rows.svelte.js +193 -0
- package/dist/components/marks/lifecycle/other-split.d.ts +80 -0
- package/dist/components/marks/lifecycle/other-split.js +112 -0
- package/dist/components/marks/lifecycle/query-builder.d.ts +26 -1
- package/dist/components/marks/lifecycle/query-builder.js +25 -5
- package/dist/components/marks/utils/reactive-config.d.ts +1 -1
- package/dist/components/marks/utils/reactive-config.js +6 -0
- package/dist/components/marks/utils/tooltip-helpers.d.ts +4 -1
- package/dist/components/marks/utils/tooltip-helpers.js +2 -1
- package/dist/components/rendering/BaseGPURenderer.svelte.d.ts +3 -1
- package/dist/components/rendering/BaseGPURenderer.svelte.js +4 -2
- package/dist/components/rendering/Rects.svelte +117 -23
- package/dist/components/rendering/Rects.svelte.d.ts +7 -0
- package/dist/components/rendering/activity-reads.d.ts +18 -0
- package/dist/components/rendering/activity-reads.js +19 -0
- package/dist/config/ChartConfig.svelte.d.ts +3 -1
- package/dist/gpu/MorphedChannels.d.ts +6 -32
- package/dist/gpu/MorphedChannels.js +45 -3
- package/dist/gpu/ScaleTemplateSystem.js +25 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/interactors/Interval1D.svelte +6 -1
- package/dist/interactors/Interval2D.svelte +6 -1
- package/dist/interactors/ParallelAxisSource.svelte +10 -1
- package/dist/interactors/Toggle.svelte +32 -3
- package/dist/interactors/Tooltip.svelte +138 -41
- package/dist/interactors/TooltipManager.svelte.d.ts +20 -5
- package/dist/interactors/TooltipManager.svelte.js +32 -7
- package/dist/interactors/other-tooltip.d.ts +16 -0
- package/dist/interactors/other-tooltip.js +22 -0
- package/dist/query-engine/FieldActivity.svelte.d.ts +50 -1
- package/dist/query-engine/FieldActivity.svelte.js +176 -5
- package/dist/query-engine/FilterGroup.svelte.d.ts +23 -1
- package/dist/query-engine/FilterGroup.svelte.js +43 -3
- package/dist/query-engine/FilterGroupRegistry.svelte.d.ts +31 -3
- package/dist/query-engine/FilterGroupRegistry.svelte.js +62 -15
- package/dist/query-engine/QueryEngine.js +26 -6
- package/dist/query-engine/categorical-identity.d.ts +15 -0
- package/dist/query-engine/categorical-identity.js +15 -0
- package/dist/query-engine/preagg/plan.d.ts +5 -0
- package/dist/query-engine/preagg/plan.js +31 -7
- package/dist/query-engine/toggle-predicate.d.ts +21 -0
- package/dist/query-engine/toggle-predicate.js +29 -1
- package/dist/scales/ActivityTable.d.ts +5 -1
- package/dist/scales/ActivityTable.js +12 -3
- package/dist/scales/BandScaleState.svelte.d.ts +13 -8
- package/dist/scales/BandScaleState.svelte.js +23 -16
- package/dist/spec/types.d.ts +2 -0
- package/dist/theme.css +5 -4
- package/dist/transforms/TransformCompiler.d.ts +37 -2
- package/dist/transforms/TransformCompiler.js +44 -1
- package/dist/transforms/index.js +32 -18
- package/dist/types.d.ts +41 -0
- package/package.json +3 -2
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a mark's Other rows hold, and what is done with it: the one owner of
|
|
3
|
+
* the three things that read an Other row's member lists.
|
|
4
|
+
*
|
|
5
|
+
* - `register`: hands the members to their field, so a click on the bucket
|
|
6
|
+
* selects them and the bucket is lit while the selection reaches into it.
|
|
7
|
+
* - `split`: a bar's selected share, for the bar renderer to draw as two parts.
|
|
8
|
+
* - `stats`: what that share is made of, for a tooltip.
|
|
9
|
+
*
|
|
10
|
+
* Takes getters, not values, so what it reads is tracked by whoever reads it.
|
|
11
|
+
* MarkLifecycleV2 holds one and says only what it is told by the mark.
|
|
12
|
+
*/
|
|
13
|
+
import type { Table } from '@uwdata/flechette';
|
|
14
|
+
import type { FilterGroup } from '../../../query-engine/FilterGroup.svelte';
|
|
15
|
+
import type { QueryEngine } from '../../../query-engine/QueryEngine';
|
|
16
|
+
import type { ChannelIdentity } from '../../../types';
|
|
17
|
+
/** What the mark is, for whether its Other bar can be split. */
|
|
18
|
+
export interface SplitPlan {
|
|
19
|
+
markType: string;
|
|
20
|
+
/** The bar's length channel: `y` for vertical bars, `x` for horizontal. */
|
|
21
|
+
length: string | undefined;
|
|
22
|
+
/** The measure topN ranks by, null for its default (the length). */
|
|
23
|
+
by: string | null;
|
|
24
|
+
/** The length channel's select expression. */
|
|
25
|
+
expr: unknown;
|
|
26
|
+
/** The mark's topN has an Other bucket. */
|
|
27
|
+
hasOther: boolean;
|
|
28
|
+
/** JS transforms follow the query, so its rows no longer line up. */
|
|
29
|
+
hasJsTransforms: boolean;
|
|
30
|
+
}
|
|
31
|
+
/** What the bar renderer draws an Other split from. */
|
|
32
|
+
export interface SplitProp {
|
|
33
|
+
/** 0 on rows that are not an Other row, `1 + share` on one. */
|
|
34
|
+
data: Float32Array;
|
|
35
|
+
/** Whether the split travels with the bar's morph or lands at once. */
|
|
36
|
+
slides: boolean;
|
|
37
|
+
/** The band axis the bars are grouped on; the split runs along the other. */
|
|
38
|
+
axis: 'x' | 'y';
|
|
39
|
+
/** Colour is the band's own field, so an Other row's colour term reads its id too. */
|
|
40
|
+
colorOnBand: boolean;
|
|
41
|
+
}
|
|
42
|
+
/** One Other row's share, in numbers. */
|
|
43
|
+
export interface OtherRowStats {
|
|
44
|
+
picked: number;
|
|
45
|
+
total: number;
|
|
46
|
+
pickedCount: number;
|
|
47
|
+
memberCount: number;
|
|
48
|
+
/** The field has a selection; without one picked is everything and says nothing. */
|
|
49
|
+
active: boolean;
|
|
50
|
+
}
|
|
51
|
+
export interface OtherRowsDeps {
|
|
52
|
+
queryEngine: QueryEngine;
|
|
53
|
+
filterGroup: FilterGroup;
|
|
54
|
+
markId: string;
|
|
55
|
+
/** The mark's result, and the version it is at. */
|
|
56
|
+
table: () => Table | null;
|
|
57
|
+
dataVersion: () => number;
|
|
58
|
+
identity: () => Record<string, ChannelIdentity> | null;
|
|
59
|
+
/** The identity key of a band axis, or null when it has none. */
|
|
60
|
+
identityKey: (axis: 'x' | 'y') => string | null;
|
|
61
|
+
/** The mark is filtered by the selection rather than dimmed by it. */
|
|
62
|
+
filtered: () => boolean;
|
|
63
|
+
/** The mark's plan, or null before its setup completes. */
|
|
64
|
+
plan: () => SplitPlan | null;
|
|
65
|
+
/** The identity key of the mark's ordinal colour scale, or null. */
|
|
66
|
+
colorKey: () => string | null;
|
|
67
|
+
/** The identity key of the chart's band scale on an axis, or null. */
|
|
68
|
+
bandScaleKey: (axis: 'x' | 'y') => string | null;
|
|
69
|
+
}
|
|
70
|
+
export declare class OtherRows {
|
|
71
|
+
#private;
|
|
72
|
+
constructor(deps: OtherRowsDeps);
|
|
73
|
+
/**
|
|
74
|
+
* Register this result's Other rows with their field: what each Other bucket
|
|
75
|
+
* holds, per facet cell, for a click on it to select (FieldActivity.setOther).
|
|
76
|
+
* Per result, so the members follow the filters the mark receives. A mark
|
|
77
|
+
* filtered by the selection (`'filter'`) registers with no group, which is
|
|
78
|
+
* how a click on its Other knows to drill down rather than toggle off.
|
|
79
|
+
*/
|
|
80
|
+
register(): void;
|
|
81
|
+
/** Give every registered row up: the mark is going away or starting over. */
|
|
82
|
+
unregister(): void;
|
|
83
|
+
/**
|
|
84
|
+
* The bar renderer's split, or null: no split, the bar is lit or dim as a
|
|
85
|
+
* whole (see #splitAxis for when a bar has one).
|
|
86
|
+
*
|
|
87
|
+
* Per row, share = Σ values[k] · selected(ids[k]) / Σ values[k] over the
|
|
88
|
+
* row's own member lists: the part of the bar the selection holds. A weighted
|
|
89
|
+
* sum over values the query already returned, in JS, per result and per
|
|
90
|
+
* selection change: the alternative is re-querying a mark that dims, which is
|
|
91
|
+
* what dimming exists to avoid.
|
|
92
|
+
*/
|
|
93
|
+
get split(): SplitProp | null;
|
|
94
|
+
/** What an Other row's share is made of, or null where it has no split. */
|
|
95
|
+
stats(row: number): OtherRowStats | null;
|
|
96
|
+
/**
|
|
97
|
+
* `stats` of the Other row a tooltip point names: its band id and, with
|
|
98
|
+
* facets, its cell (null: any). A tooltip's rows are not the result's, so it
|
|
99
|
+
* finds its row by identity. A scan of the result's rows, per hover change
|
|
100
|
+
* and not per frame; null where the point is not an Other row of this mark's
|
|
101
|
+
* split.
|
|
102
|
+
*/
|
|
103
|
+
statsAt(bandId: number, facetId: number | null): OtherRowStats | null;
|
|
104
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a mark's Other rows hold, and what is done with it: the one owner of
|
|
3
|
+
* the three things that read an Other row's member lists.
|
|
4
|
+
*
|
|
5
|
+
* - `register`: hands the members to their field, so a click on the bucket
|
|
6
|
+
* selects them and the bucket is lit while the selection reaches into it.
|
|
7
|
+
* - `split`: a bar's selected share, for the bar renderer to draw as two parts.
|
|
8
|
+
* - `stats`: what that share is made of, for a tooltip.
|
|
9
|
+
*
|
|
10
|
+
* Takes getters, not values, so what it reads is tracked by whoever reads it.
|
|
11
|
+
* MarkLifecycleV2 holds one and says only what it is told by the mark.
|
|
12
|
+
*/
|
|
13
|
+
import { OTHER_IDS_COLUMN, OTHER_VALUES_COLUMN } from '../../../transforms';
|
|
14
|
+
import { otherSplit, splittable } from './other-split';
|
|
15
|
+
export class OtherRows {
|
|
16
|
+
#deps;
|
|
17
|
+
/** The (field, Other id) pairs this mark has rows registered under. */
|
|
18
|
+
#registered = new Map();
|
|
19
|
+
/** The split last computed, and for which result: what a selection change holds against. */
|
|
20
|
+
#last = null;
|
|
21
|
+
constructor(deps) {
|
|
22
|
+
this.#deps = deps;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Register this result's Other rows with their field: what each Other bucket
|
|
26
|
+
* holds, per facet cell, for a click on it to select (FieldActivity.setOther).
|
|
27
|
+
* Per result, so the members follow the filters the mark receives. A mark
|
|
28
|
+
* filtered by the selection (`'filter'`) registers with no group, which is
|
|
29
|
+
* how a click on its Other knows to drill down rather than toggle off.
|
|
30
|
+
*/
|
|
31
|
+
register() {
|
|
32
|
+
const { table: getTable, identity: getIdentity, filterGroup, markId } = this.#deps;
|
|
33
|
+
const table = getTable();
|
|
34
|
+
const identity = getIdentity();
|
|
35
|
+
const group = this.#deps.filtered() ? null : filterGroup.index;
|
|
36
|
+
const now = new Map();
|
|
37
|
+
const ids = table?.getChild(OTHER_IDS_COLUMN) ?? null;
|
|
38
|
+
if (table && identity && ids) {
|
|
39
|
+
const facets = table.getChild('__facet');
|
|
40
|
+
for (const axis of ['x', 'y']) {
|
|
41
|
+
const otherId = identity[axis]?.otherId;
|
|
42
|
+
const key = this.#deps.identityKey(axis);
|
|
43
|
+
const band = table.getChild(axis);
|
|
44
|
+
if (otherId == null || !key || !band)
|
|
45
|
+
continue;
|
|
46
|
+
const rows = [];
|
|
47
|
+
for (let i = 0; i < table.numRows; i++) {
|
|
48
|
+
if (Number(band.at(i)) !== otherId)
|
|
49
|
+
continue;
|
|
50
|
+
const members = ids.at(i);
|
|
51
|
+
if (!members)
|
|
52
|
+
continue;
|
|
53
|
+
rows.push({ facetId: facets ? Number(facets.at(i)) : null, ids: members });
|
|
54
|
+
}
|
|
55
|
+
filterGroup.activity(key).setOther(otherId, markId, { group, rows });
|
|
56
|
+
now.set(key, otherId);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// A field or bucket this mark no longer draws an Other on gives its rows up.
|
|
60
|
+
for (const [key, otherId] of this.#registered) {
|
|
61
|
+
if (now.get(key) !== otherId)
|
|
62
|
+
filterGroup.activity(key).setOther(otherId, markId, null);
|
|
63
|
+
}
|
|
64
|
+
this.#registered = now;
|
|
65
|
+
}
|
|
66
|
+
/** Give every registered row up: the mark is going away or starting over. */
|
|
67
|
+
unregister() {
|
|
68
|
+
for (const [key, otherId] of this.#registered) {
|
|
69
|
+
this.#deps.filterGroup.activity(key).setOther(otherId, this.#deps.markId, null);
|
|
70
|
+
}
|
|
71
|
+
this.#registered = new Map();
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The bar renderer's split, or null: no split, the bar is lit or dim as a
|
|
75
|
+
* whole (see #splitAxis for when a bar has one).
|
|
76
|
+
*
|
|
77
|
+
* Per row, share = Σ values[k] · selected(ids[k]) / Σ values[k] over the
|
|
78
|
+
* row's own member lists: the part of the bar the selection holds. A weighted
|
|
79
|
+
* sum over values the query already returned, in JS, per result and per
|
|
80
|
+
* selection change: the alternative is re-querying a mark that dims, which is
|
|
81
|
+
* what dimming exists to avoid.
|
|
82
|
+
*/
|
|
83
|
+
get split() {
|
|
84
|
+
const axis = this.#splitAxis;
|
|
85
|
+
const computed = this.#computed;
|
|
86
|
+
if (!axis || !computed)
|
|
87
|
+
return null;
|
|
88
|
+
return {
|
|
89
|
+
data: computed.split,
|
|
90
|
+
slides: computed.slides,
|
|
91
|
+
axis,
|
|
92
|
+
colorOnBand: this.#colorOnBand(axis)
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/** What an Other row's share is made of, or null where it has no split. */
|
|
96
|
+
stats(row) {
|
|
97
|
+
const computed = this.#computed;
|
|
98
|
+
if (!computed || computed.memberCount[row] === 0)
|
|
99
|
+
return null;
|
|
100
|
+
return {
|
|
101
|
+
picked: computed.picked[row],
|
|
102
|
+
total: computed.total[row],
|
|
103
|
+
pickedCount: computed.pickedCount[row],
|
|
104
|
+
memberCount: computed.memberCount[row],
|
|
105
|
+
active: computed.active
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* `stats` of the Other row a tooltip point names: its band id and, with
|
|
110
|
+
* facets, its cell (null: any). A tooltip's rows are not the result's, so it
|
|
111
|
+
* finds its row by identity. A scan of the result's rows, per hover change
|
|
112
|
+
* and not per frame; null where the point is not an Other row of this mark's
|
|
113
|
+
* split.
|
|
114
|
+
*/
|
|
115
|
+
statsAt(bandId, facetId) {
|
|
116
|
+
const axis = this.#splitAxis;
|
|
117
|
+
const table = this.#deps.table();
|
|
118
|
+
const otherId = axis ? this.#deps.identity()?.[axis]?.otherId : null;
|
|
119
|
+
if (!axis || !table || otherId == null || bandId !== otherId)
|
|
120
|
+
return null;
|
|
121
|
+
const band = table.getChild(axis);
|
|
122
|
+
const facets = facetId == null ? null : table.getChild('__facet');
|
|
123
|
+
if (!band)
|
|
124
|
+
return null;
|
|
125
|
+
for (let row = 0; row < table.numRows; row++) {
|
|
126
|
+
if (Number(band.at(row)) !== bandId)
|
|
127
|
+
continue;
|
|
128
|
+
if (facets && Number(facets.at(row)) !== facetId)
|
|
129
|
+
continue;
|
|
130
|
+
return this.stats(row);
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The band axis whose Other bar splits, or null. Only where a share is what
|
|
136
|
+
* it looks like:
|
|
137
|
+
* - a bar mark ('bary', 'barx') with a topN Other bucket, dimming (not
|
|
138
|
+
* 'filter', which never dims) and without JS transforms, whose rows would
|
|
139
|
+
* no longer line up with the query's;
|
|
140
|
+
* - whose length is the measure topN ranks by, which is what Other sums;
|
|
141
|
+
* - and that measure a bare COUNT or SUM, or a raw column: a mean or a max
|
|
142
|
+
* summed into Other is no total, and a share of it would mean nothing.
|
|
143
|
+
*/
|
|
144
|
+
get #splitAxis() {
|
|
145
|
+
const plan = this.#deps.plan();
|
|
146
|
+
if (!plan || this.#deps.filtered())
|
|
147
|
+
return null;
|
|
148
|
+
if (!plan.hasOther || plan.hasJsTransforms)
|
|
149
|
+
return null;
|
|
150
|
+
if (plan.markType !== 'bary' && plan.markType !== 'barx')
|
|
151
|
+
return null;
|
|
152
|
+
return splittable(plan.length, plan.by, plan.expr)
|
|
153
|
+
? plan.markType === 'barx'
|
|
154
|
+
? 'y'
|
|
155
|
+
: 'x'
|
|
156
|
+
: null;
|
|
157
|
+
}
|
|
158
|
+
/** Whether colour is the band's own field, so an Other row's colour term reads its id too. */
|
|
159
|
+
#colorOnBand(axis) {
|
|
160
|
+
const color = this.#deps.colorKey();
|
|
161
|
+
return color != null && color === this.#deps.bandScaleKey(axis);
|
|
162
|
+
}
|
|
163
|
+
#computed = $derived.by(() => {
|
|
164
|
+
const axis = this.#splitAxis;
|
|
165
|
+
if (!axis)
|
|
166
|
+
return null;
|
|
167
|
+
const dataVersion = this.#deps.dataVersion();
|
|
168
|
+
const table = this.#deps.table();
|
|
169
|
+
const otherId = this.#deps.identity()?.[axis]?.otherId;
|
|
170
|
+
const key = this.#deps.identityKey(axis);
|
|
171
|
+
const ids = table?.getChild(OTHER_IDS_COLUMN) ?? null;
|
|
172
|
+
const values = table?.getChild(OTHER_VALUES_COLUMN) ?? null;
|
|
173
|
+
const band = table?.getChild(axis) ?? null;
|
|
174
|
+
if (!table || otherId == null || !key || !ids || !values || !band)
|
|
175
|
+
return null;
|
|
176
|
+
// The last split, for the same result only: a selection change holds a
|
|
177
|
+
// flat row's position. A new result starts afresh and travels with the bar.
|
|
178
|
+
const { filterGroup, queryEngine } = this.#deps;
|
|
179
|
+
const last = this.#last;
|
|
180
|
+
const next = otherSplit({
|
|
181
|
+
rows: table.numRows,
|
|
182
|
+
band,
|
|
183
|
+
otherId,
|
|
184
|
+
ids,
|
|
185
|
+
values,
|
|
186
|
+
selected: filterGroup.activity(key).selectedValues(filterGroup.index),
|
|
187
|
+
decode: queryEngine.identityFor(key).values,
|
|
188
|
+
previous: last && last.dataVersion === dataVersion ? last.split : null
|
|
189
|
+
});
|
|
190
|
+
this.#last = { dataVersion, split: next };
|
|
191
|
+
return next;
|
|
192
|
+
});
|
|
193
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An Other bar's selected share, per row, and when a bar has one: the pure
|
|
3
|
+
* half of MarkLifecycleV2's `otherSplit`, so it can be tested without a mark.
|
|
4
|
+
*/
|
|
5
|
+
/** A column of a result, as Arrow (flechette) hands it: one value per row. */
|
|
6
|
+
export interface RowColumn {
|
|
7
|
+
at(row: number): unknown;
|
|
8
|
+
}
|
|
9
|
+
/** One computed split: the channel, and which Other rows are flat. */
|
|
10
|
+
export interface OtherSplit {
|
|
11
|
+
/** 0 on rows that are not an Other row, `1 + share` (or a held position) on one. */
|
|
12
|
+
split: Float32Array;
|
|
13
|
+
/**
|
|
14
|
+
* 1 on an Other row whose two parts look the same: nothing selected (both
|
|
15
|
+
* lit), or none of its members selected (both dim). Its split position is
|
|
16
|
+
* invisible there, so it is held, and moving out of it lands rather than
|
|
17
|
+
* slides.
|
|
18
|
+
*/
|
|
19
|
+
flat: Uint8Array;
|
|
20
|
+
/**
|
|
21
|
+
* Whether the split travels with this write (the renderer morphs it) or
|
|
22
|
+
* lands at once. It lands when a row leaves a flat state: both sides of it
|
|
23
|
+
* look the same at that moment, so a slide would be a movement the eye
|
|
24
|
+
* reads as meaning something; the fades show the change instead.
|
|
25
|
+
*/
|
|
26
|
+
slides: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* What the share is made of, per row (0 on a row with no split), for a
|
|
29
|
+
* tooltip to say: `picked` of `total` (Σ values over the selected members,
|
|
30
|
+
* over all), `pickedCount` of `memberCount` categories. With nothing
|
|
31
|
+
* selected everything counts as picked.
|
|
32
|
+
*/
|
|
33
|
+
picked: Float64Array;
|
|
34
|
+
total: Float64Array;
|
|
35
|
+
pickedCount: Int32Array;
|
|
36
|
+
memberCount: Int32Array;
|
|
37
|
+
/** The field has a selection: without one, everything counts as picked, which says nothing. */
|
|
38
|
+
active: boolean;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The split channel for the bar renderer, per row: share = Σ values · selected
|
|
42
|
+
* / Σ values over the row's own member lists. Selection is by value (`decode`
|
|
43
|
+
* maps an id to its value), so the NULL category needs nothing special. A row
|
|
44
|
+
* whose members mix signs gets no split (0): a share of a sum that cancels
|
|
45
|
+
* means nothing.
|
|
46
|
+
*
|
|
47
|
+
* `previous`: the last split for the SAME rows (the same result), when only the
|
|
48
|
+
* selection changed. A flat row then holds its previous position, so going flat
|
|
49
|
+
* (a selection cleared, or one that no longer reaches into Other) is a fade,
|
|
50
|
+
* not a slide. Without it (a new result) every row takes its value, and the
|
|
51
|
+
* split travels with the bar's own morph.
|
|
52
|
+
*
|
|
53
|
+
* Cost, measured: about 22 ns per Other member per call (the `selected.has`),
|
|
54
|
+
* so 0.1 ms at 5 000 members and about 1.2 ms at 50 000. Fine per click. If
|
|
55
|
+
* selection ever changes continuously (a brush on a band axis) over a very large
|
|
56
|
+
* Other, test membership against a Uint8Array indexed by id, built once per
|
|
57
|
+
* change, rather than a Set. The arrays allocated here are sized by rows.
|
|
58
|
+
*/
|
|
59
|
+
export declare function otherSplit(opts: {
|
|
60
|
+
rows: number;
|
|
61
|
+
band: RowColumn;
|
|
62
|
+
otherId: number;
|
|
63
|
+
ids: RowColumn;
|
|
64
|
+
values: RowColumn;
|
|
65
|
+
selected: ReadonlySet<unknown> | null;
|
|
66
|
+
decode: ArrayLike<unknown>;
|
|
67
|
+
previous?: OtherSplit | null;
|
|
68
|
+
}): OtherSplit;
|
|
69
|
+
/**
|
|
70
|
+
* Whether a bar's Other bucket can be split: its length must be what Other
|
|
71
|
+
* sums, and a total. `length` is the bar's length channel, `by` the channel
|
|
72
|
+
* topN ranks by (null: its default, the length), `expr` the length channel's
|
|
73
|
+
* select expression.
|
|
74
|
+
*
|
|
75
|
+
* Other sums `by`, so a length channel that is not `by` is 0 on the Other row.
|
|
76
|
+
* A bare COUNT or SUM, or a raw column (an ungrouped mark), is a total; a mean,
|
|
77
|
+
* a max, a ratio or a distinct count summed into Other is not, and a share of
|
|
78
|
+
* it would look exact while meaning nothing.
|
|
79
|
+
*/
|
|
80
|
+
export declare function splittable(length: string | undefined, by: string | null, expr: unknown): boolean;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An Other bar's selected share, per row, and when a bar has one: the pure
|
|
3
|
+
* half of MarkLifecycleV2's `otherSplit`, so it can be tested without a mark.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* The split channel for the bar renderer, per row: share = Σ values · selected
|
|
7
|
+
* / Σ values over the row's own member lists. Selection is by value (`decode`
|
|
8
|
+
* maps an id to its value), so the NULL category needs nothing special. A row
|
|
9
|
+
* whose members mix signs gets no split (0): a share of a sum that cancels
|
|
10
|
+
* means nothing.
|
|
11
|
+
*
|
|
12
|
+
* `previous`: the last split for the SAME rows (the same result), when only the
|
|
13
|
+
* selection changed. A flat row then holds its previous position, so going flat
|
|
14
|
+
* (a selection cleared, or one that no longer reaches into Other) is a fade,
|
|
15
|
+
* not a slide. Without it (a new result) every row takes its value, and the
|
|
16
|
+
* split travels with the bar's own morph.
|
|
17
|
+
*
|
|
18
|
+
* Cost, measured: about 22 ns per Other member per call (the `selected.has`),
|
|
19
|
+
* so 0.1 ms at 5 000 members and about 1.2 ms at 50 000. Fine per click. If
|
|
20
|
+
* selection ever changes continuously (a brush on a band axis) over a very large
|
|
21
|
+
* Other, test membership against a Uint8Array indexed by id, built once per
|
|
22
|
+
* change, rather than a Set. The arrays allocated here are sized by rows.
|
|
23
|
+
*/
|
|
24
|
+
export function otherSplit(opts) {
|
|
25
|
+
const { rows, band, otherId, ids, values, selected, decode } = opts;
|
|
26
|
+
const previous = opts.previous && opts.previous.split.length === rows ? opts.previous : null;
|
|
27
|
+
const split = new Float32Array(rows);
|
|
28
|
+
const flat = new Uint8Array(rows);
|
|
29
|
+
const pickedSums = new Float64Array(rows);
|
|
30
|
+
const totals = new Float64Array(rows);
|
|
31
|
+
const pickedCount = new Int32Array(rows);
|
|
32
|
+
const memberCount = new Int32Array(rows);
|
|
33
|
+
let slides = true;
|
|
34
|
+
for (let row = 0; row < rows; row++) {
|
|
35
|
+
if (Number(band.at(row)) !== otherId)
|
|
36
|
+
continue;
|
|
37
|
+
const memberIds = ids.at(row);
|
|
38
|
+
const memberValues = values.at(row);
|
|
39
|
+
if (!memberIds || !memberValues)
|
|
40
|
+
continue;
|
|
41
|
+
let total = 0;
|
|
42
|
+
let picked = 0;
|
|
43
|
+
let count = 0;
|
|
44
|
+
let positive = false;
|
|
45
|
+
let negative = false;
|
|
46
|
+
for (let k = 0; k < memberIds.length; k++) {
|
|
47
|
+
const v = memberValues[k];
|
|
48
|
+
if (v > 0)
|
|
49
|
+
positive = true;
|
|
50
|
+
else if (v < 0)
|
|
51
|
+
negative = true;
|
|
52
|
+
total += v;
|
|
53
|
+
if (selected == null || selected.has(decode[memberIds[k]])) {
|
|
54
|
+
picked += v;
|
|
55
|
+
count++;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (positive && negative)
|
|
59
|
+
continue;
|
|
60
|
+
pickedSums[row] = picked;
|
|
61
|
+
totals[row] = total;
|
|
62
|
+
pickedCount[row] = count;
|
|
63
|
+
memberCount[row] = memberIds.length;
|
|
64
|
+
const share = total === 0 ? (selected == null ? 1 : 0) : Math.min(1, Math.max(0, picked / total));
|
|
65
|
+
const isFlat = selected == null || share === 0;
|
|
66
|
+
flat[row] = isFlat ? 1 : 0;
|
|
67
|
+
if (isFlat && previous) {
|
|
68
|
+
const held = previous.split[row];
|
|
69
|
+
split[row] = held > 0.5 ? held : 2;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
split[row] = 1 + share;
|
|
73
|
+
}
|
|
74
|
+
if (previous && previous.flat[row] === 1 && !isFlat)
|
|
75
|
+
slides = false;
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
split,
|
|
79
|
+
flat,
|
|
80
|
+
slides,
|
|
81
|
+
picked: pickedSums,
|
|
82
|
+
total: totals,
|
|
83
|
+
pickedCount,
|
|
84
|
+
memberCount,
|
|
85
|
+
active: selected != null
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Whether a bar's Other bucket can be split: its length must be what Other
|
|
90
|
+
* sums, and a total. `length` is the bar's length channel, `by` the channel
|
|
91
|
+
* topN ranks by (null: its default, the length), `expr` the length channel's
|
|
92
|
+
* select expression.
|
|
93
|
+
*
|
|
94
|
+
* Other sums `by`, so a length channel that is not `by` is 0 on the Other row.
|
|
95
|
+
* A bare COUNT or SUM, or a raw column (an ungrouped mark), is a total; a mean,
|
|
96
|
+
* a max, a ratio or a distinct count summed into Other is not, and a share of
|
|
97
|
+
* it would look exact while meaning nothing.
|
|
98
|
+
*/
|
|
99
|
+
export function splittable(length, by, expr) {
|
|
100
|
+
if (!length || (by ?? length) !== length)
|
|
101
|
+
return false;
|
|
102
|
+
if (typeof expr === 'string')
|
|
103
|
+
return true;
|
|
104
|
+
const node = expr;
|
|
105
|
+
if (!node || typeof node !== 'object')
|
|
106
|
+
return false;
|
|
107
|
+
if (node.type === 'COLUMN_REF')
|
|
108
|
+
return true;
|
|
109
|
+
if (node.type !== 'AGGREGATE' || node.isDistinct)
|
|
110
|
+
return false;
|
|
111
|
+
return node.name === 'count' || node.name === 'sum';
|
|
112
|
+
}
|
|
@@ -26,7 +26,31 @@ export interface QueryParts {
|
|
|
26
26
|
* CAST for ORDER BY, ordinal CASE for PARTITION BY). Used by preagg to
|
|
27
27
|
* fold redundant window col refs into channel aliases. */
|
|
28
28
|
passthroughChannels: Map<string, string>;
|
|
29
|
+
/**
|
|
30
|
+
* `nulls="drop"`: the categorical channels whose NULL the mark leaves out.
|
|
31
|
+
* Stated by CHANNEL, not by source column, so every path filters in the
|
|
32
|
+
* terms of its own select: `assembleQuery` reads each channel's expression
|
|
33
|
+
* from the `selectDef` it is given — the id column on the base table, the
|
|
34
|
+
* preagg table's column when preagg rewrote it — and ANDs the predicate
|
|
35
|
+
* into the base scan, before any transform.
|
|
36
|
+
*/
|
|
37
|
+
nullDrops?: readonly NullDrop[];
|
|
29
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* One channel whose NULL a mark drops. With `nullId`, the channel carries dim
|
|
41
|
+
* ids and the NULL category is that id: `<channel> <> nullId`. Without (a raw
|
|
42
|
+
* value, or a dim with no NULL row): `<channel> IS NOT NULL`.
|
|
43
|
+
*/
|
|
44
|
+
export interface NullDrop {
|
|
45
|
+
channel: string;
|
|
46
|
+
nullId: number | null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The predicates for `drops`, each on its channel's expression in `selectDef`.
|
|
50
|
+
* A dropped channel must be a row-level group key of the query; one the query
|
|
51
|
+
* does not select would be left unfiltered, so that is an error, not a skip.
|
|
52
|
+
*/
|
|
53
|
+
export declare function nullDropPredicates(drops: readonly NullDrop[], selectDef: Record<string, unknown>): ExprNode[];
|
|
30
54
|
/** Window functions to apply in an outer CTE over the assembled inner query. */
|
|
31
55
|
export interface WindowWrap {
|
|
32
56
|
/** channel → outer WindowNode (already rewritten to reference channel aliases). */
|
|
@@ -38,9 +62,10 @@ export interface AssembleOptions {
|
|
|
38
62
|
parts: QueryParts;
|
|
39
63
|
/** Per-call filter predicates (crossfilter, bin filters). */
|
|
40
64
|
filter?: unknown[];
|
|
41
|
-
/** Columns to add IS NOT NULL predicates for. */
|
|
42
65
|
transformCompiler: TransformCompiler;
|
|
43
66
|
limit?: number | null;
|
|
67
|
+
/** The table already has the mark's row filters (`filter()`) applied: a preagg table. */
|
|
68
|
+
skipRowFilters?: boolean;
|
|
44
69
|
/** When set, the inner query is wrapped in a CTE and these window
|
|
45
70
|
* functions are applied in the outer SELECT. ORDER BY / LIMIT move to
|
|
46
71
|
* the outer query. */
|
|
@@ -1,5 +1,22 @@
|
|
|
1
|
-
import { isNode, isAggregateExpression, isColumnRef, isColumnParam, column as sqlColumn, cast, coalesce, collectColumns, rewrite, sql, isNotNull, and, sum, avg, min, max, Query, WindowNode } from '@uwdata/mosaic-sql';
|
|
1
|
+
import { isNode, isAggregateExpression, isColumnRef, isColumnParam, column as sqlColumn, cast, coalesce, collectColumns, rewrite, sql, isNotNull, neq, literal, and, sum, avg, min, max, Query, WindowNode } from '@uwdata/mosaic-sql';
|
|
2
2
|
import { isWindowExpression, isVerbatimWindowExpression } from '../../../sql/expression-utils';
|
|
3
|
+
/**
|
|
4
|
+
* The predicates for `drops`, each on its channel's expression in `selectDef`.
|
|
5
|
+
* A dropped channel must be a row-level group key of the query; one the query
|
|
6
|
+
* does not select would be left unfiltered, so that is an error, not a skip.
|
|
7
|
+
*/
|
|
8
|
+
export function nullDropPredicates(drops, selectDef) {
|
|
9
|
+
const out = [];
|
|
10
|
+
for (const { channel, nullId } of drops) {
|
|
11
|
+
const value = selectDef[channel];
|
|
12
|
+
if (value === undefined) {
|
|
13
|
+
throw new Error(`nulls="drop" filters the "${channel}" channel, which this query does not select.`);
|
|
14
|
+
}
|
|
15
|
+
const expr = (typeof value === 'string' ? sqlColumn(value) : value);
|
|
16
|
+
out.push(nullId === null ? isNotNull(expr) : neq(expr, literal(nullId)));
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
3
20
|
// ---------------------------------------------------------------------------
|
|
4
21
|
// Numeric CAST policy (single rule for every assembly path)
|
|
5
22
|
// ---------------------------------------------------------------------------
|
|
@@ -208,7 +225,7 @@ export function buildQueryParts(select, orderConfig, hasSQLTransforms, passthrou
|
|
|
208
225
|
* Order is fixed and shared by the flat, window-CTE, and preagg paths:
|
|
209
226
|
*
|
|
210
227
|
* SELECT parts.selectDef FROM table [GROUP BY dims]
|
|
211
|
-
* WHERE filter AND
|
|
228
|
+
* WHERE filter AND nullDrops — on the base scan, so transform CTEs
|
|
212
229
|
* (topN ranking etc.) see filtered rows
|
|
213
230
|
* → SQL transforms (CTE wrapping)
|
|
214
231
|
* → [window CTE: outer SELECT applies window fns over the inner result]
|
|
@@ -267,19 +284,22 @@ export function outerOrder(preference, alignment) {
|
|
|
267
284
|
return order;
|
|
268
285
|
}
|
|
269
286
|
export function assembleQuery(opts) {
|
|
270
|
-
const { table, parts, filter = [], transformCompiler, limit, window } = opts;
|
|
287
|
+
const { table, parts, filter = [], transformCompiler, limit, window, skipRowFilters } = opts;
|
|
271
288
|
const { selectDef, nonAggregateGroupBy, hasAggregates, hasSQLTransforms, orderChannels } = parts;
|
|
272
289
|
const q = Query.from(table).select(selectDef);
|
|
273
290
|
if (hasAggregates && nonAggregateGroupBy.length > 0) {
|
|
274
291
|
q.groupby(nonAggregateGroupBy);
|
|
275
292
|
}
|
|
276
|
-
const allFilters = [
|
|
293
|
+
const allFilters = [
|
|
294
|
+
...filter,
|
|
295
|
+
...nullDropPredicates(parts.nullDrops ?? [], selectDef)
|
|
296
|
+
];
|
|
277
297
|
if (allFilters.length > 0) {
|
|
278
298
|
q.where(and(allFilters));
|
|
279
299
|
}
|
|
280
300
|
let result = q;
|
|
281
301
|
if (hasSQLTransforms) {
|
|
282
|
-
result = transformCompiler.applyToQuery(q, { groupKeys: parts.groupKeys });
|
|
302
|
+
result = transformCompiler.applyToQuery(q, { groupKeys: parts.groupKeys }, { skipRowFilters });
|
|
283
303
|
}
|
|
284
304
|
if (window) {
|
|
285
305
|
const outerSelect = {};
|
|
@@ -12,6 +12,6 @@ import type { MarkEntry } from '../../../config/ChartConfig.svelte';
|
|
|
12
12
|
* @param entry - The mark entry (reactive props from ChartConfig)
|
|
13
13
|
* @param opts.resolveFitDomain - Getter for resolved fitDomain (deferred read)
|
|
14
14
|
*/
|
|
15
|
-
export declare function buildReactiveConfig(entry: Partial<Pick<MarkEntry, 'sort' | 'limit' | 'domain' | 'tooltip' | 'tip' | 'fitDomain' | 'selectionResponse'>>, opts?: {
|
|
15
|
+
export declare function buildReactiveConfig(entry: Partial<Pick<MarkEntry, 'sort' | 'limit' | 'domain' | 'tooltip' | 'tip' | 'fitDomain' | 'selectionResponse' | 'nulls' | 'contributeDomain'>>, opts?: {
|
|
16
16
|
resolveFitDomain?: () => 'x' | 'y' | boolean | undefined;
|
|
17
17
|
}): ReactiveConfig;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { OtherRowStats } from '../lifecycle/other-rows.svelte';
|
|
1
2
|
import type { ChartContext } from '../../../types';
|
|
2
3
|
import type { ColorScaleState } from '../../../scales/ColorScaleState.svelte';
|
|
3
4
|
import type { DimValue } from '../../../query-engine/categorical-identity';
|
|
@@ -34,6 +35,7 @@ interface TooltipConfigParams {
|
|
|
34
35
|
constantRadius?: number;
|
|
35
36
|
sizeScaleState?: SizeScaleState | null;
|
|
36
37
|
displayFields?: string[] | null;
|
|
38
|
+
otherStats?: TooltipConfig['otherStats'];
|
|
37
39
|
}
|
|
38
40
|
interface TooltipConfig {
|
|
39
41
|
tempTable: string;
|
|
@@ -61,7 +63,8 @@ interface TooltipConfig {
|
|
|
61
63
|
constantRadius?: number;
|
|
62
64
|
sizeScaleState?: SizeScaleState | null;
|
|
63
65
|
displayFields?: string[] | null;
|
|
66
|
+
otherStats?: (point: Record<string, unknown>) => OtherRowStats | null;
|
|
64
67
|
enabled: boolean;
|
|
65
68
|
}
|
|
66
|
-
export declare function buildTooltipConfig({ tempTable, markType, chartContext, channelFields, columns, bandAxes, colorColumn, markColor, seriesColumn, markSeries, proximityAxis, constantRadius, sizeScaleState, displayFields }: TooltipConfigParams): TooltipConfig;
|
|
69
|
+
export declare function buildTooltipConfig({ tempTable, markType, chartContext, channelFields, columns, bandAxes, colorColumn, markColor, seriesColumn, markSeries, proximityAxis, constantRadius, sizeScaleState, displayFields, otherStats }: TooltipConfigParams): TooltipConfig;
|
|
67
70
|
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export function buildTooltipConfig({ tempTable, markType, chartContext, channelFields, columns, bandAxes, colorColumn, markColor, seriesColumn, markSeries, proximityAxis, constantRadius, sizeScaleState, displayFields }) {
|
|
1
|
+
export function buildTooltipConfig({ tempTable, markType, chartContext, channelFields, columns, bandAxes, colorColumn, markColor, seriesColumn, markSeries, proximityAxis, constantRadius, sizeScaleState, displayFields, otherStats }) {
|
|
2
2
|
const colorScaleState = markColor.scaleState;
|
|
3
3
|
const constantColor = markColor.constantCssColor;
|
|
4
4
|
return {
|
|
@@ -33,6 +33,7 @@ export function buildTooltipConfig({ tempTable, markType, chartContext, channelF
|
|
|
33
33
|
...(constantRadius != null && { constantRadius }),
|
|
34
34
|
...(sizeScaleState && { sizeScaleState }),
|
|
35
35
|
...(displayFields && { displayFields }),
|
|
36
|
+
...(otherStats && { otherStats }),
|
|
36
37
|
enabled: true
|
|
37
38
|
};
|
|
38
39
|
}
|
|
@@ -57,7 +57,9 @@ export declare class BaseGPURenderer {
|
|
|
57
57
|
* the band axis's scale is whose clock these channels adopt. See
|
|
58
58
|
* MorphedChannels for the fold, the snap and the clock.
|
|
59
59
|
*/
|
|
60
|
-
writeMorphed(bandAxis: 'x' | 'y', bandIds: ArrayLike<number>, channels: readonly (readonly [number, AllowSharedBufferSource])[]
|
|
60
|
+
writeMorphed(bandAxis: 'x' | 'y', bandIds: ArrayLike<number>, channels: readonly (readonly [number, AllowSharedBufferSource])[],
|
|
61
|
+
/** Bindings that land at once rather than travel (see MorphedChannels.write). */
|
|
62
|
+
land?: readonly number[]): void;
|
|
61
63
|
/**
|
|
62
64
|
* Write data to a uniform binding.
|
|
63
65
|
*/
|