ggaction 0.0.4 → 0.0.6
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/CHANGELOG.md +55 -0
- package/README.md +35 -13
- package/package.json +18 -7
- package/src/actions/boxPlots/create.js +13 -7
- package/src/actions/boxPlots/edit.js +484 -36
- package/src/actions/boxPlots/materialize.js +3 -0
- package/src/actions/boxPlots/options.js +11 -4
- package/src/actions/boxPlots/resolve.js +10 -2
- package/src/actions/charts/bar.js +50 -0
- package/src/actions/charts/heatmap.js +205 -0
- package/src/actions/charts/histogram.js +58 -0
- package/src/actions/charts/index.js +15 -0
- package/src/actions/charts/line.js +69 -0
- package/src/actions/charts/parallel.js +76 -0
- package/src/actions/charts/scatter.js +56 -0
- package/src/actions/charts/shared.js +121 -0
- package/src/actions/composition/actions.js +13 -3
- package/src/actions/coordinates/parallel.js +50 -0
- package/src/actions/data/bin2d.js +260 -0
- package/src/actions/data/density.js +54 -13
- package/src/actions/data/derived.js +22 -0
- package/src/actions/data/filter.js +5 -0
- package/src/actions/data/gradientProfile.js +65 -0
- package/src/actions/data/horizon.js +71 -0
- package/src/actions/data/index.js +34 -2
- package/src/actions/data/intervalEdit.js +76 -0
- package/src/actions/data/window.js +50 -0
- package/src/actions/distributions/revision.js +326 -0
- package/src/actions/encodings/appearance.js +35 -1
- package/src/actions/encodings/color/index.js +17 -1
- package/src/actions/encodings/color/layout.js +8 -0
- package/src/actions/encodings/density/resolve.js +109 -0
- package/src/actions/encodings/density.js +330 -90
- package/src/actions/encodings/horizon/resolve.js +283 -0
- package/src/actions/encodings/horizon.js +364 -0
- package/src/actions/encodings/index.js +8 -0
- package/src/actions/encodings/parallel.js +115 -0
- package/src/actions/encodings/pathOrder.js +131 -0
- package/src/actions/encodings/position/apply.js +73 -4
- package/src/actions/encodings/position/index.js +15 -2
- package/src/actions/encodings/position/policies/arc.js +27 -4
- package/src/actions/encodings/position/policies/area.js +33 -2
- package/src/actions/encodings/position/policies/index.js +7 -2
- package/src/actions/encodings/position/policies/line.js +18 -2
- package/src/actions/encodings/position/resolve.js +24 -2
- package/src/actions/encodings/ranged.js +3 -1
- package/src/actions/encodings/remove.js +268 -0
- package/src/actions/encodings/ruleAppearance.js +88 -10
- package/src/actions/errorBands/edit.js +121 -13
- package/src/actions/errorBars/edit.js +45 -8
- package/src/actions/errorBars/options.js +7 -1
- package/src/actions/facets/actions.js +137 -3
- package/src/actions/facets/derive.js +33 -3
- package/src/actions/facets/replay.js +7 -36
- package/src/actions/gradientPlots/components.js +177 -0
- package/src/actions/gradientPlots/create.js +96 -0
- package/src/actions/gradientPlots/edit.js +501 -0
- package/src/actions/gradientPlots/index.js +24 -0
- package/src/actions/gradientPlots/materialize.js +287 -0
- package/src/actions/gradientPlots/options.js +120 -0
- package/src/actions/gradientPlots/paint.js +107 -0
- package/src/actions/gradientPlots/rebind.js +51 -0
- package/src/actions/gradientPlots/resolve.js +81 -0
- package/src/actions/guides/applicability.js +44 -17
- package/src/actions/guides/axes/axes.js +37 -3
- package/src/actions/guides/axes/edit.js +129 -47
- package/src/actions/guides/axes/index.js +2 -0
- package/src/actions/guides/axes/labels.js +11 -6
- package/src/actions/guides/axes/parallel/resolve.js +86 -0
- package/src/actions/guides/axes/parallel.js +228 -0
- package/src/actions/guides/axes/titles.js +11 -8
- package/src/actions/guides/guides.js +60 -5
- package/src/actions/guides/legends/categorical/actions.js +67 -19
- package/src/actions/guides/legends/continuous/common.js +3 -3
- package/src/actions/guides/legends/edit.js +63 -0
- package/src/actions/guides/legends/index.js +2 -0
- package/src/actions/guides/legends/remove.js +124 -30
- package/src/actions/guides/legends/strokeWidth.js +198 -0
- package/src/actions/index.js +6 -0
- package/src/actions/marks/arc/actions.js +46 -14
- package/src/actions/marks/area/actions.js +60 -136
- package/src/actions/marks/area/materialize.js +163 -0
- package/src/actions/marks/index.js +5 -1
- package/src/actions/marks/line/actions.js +99 -80
- package/src/actions/marks/line/materialize.js +146 -0
- package/src/actions/marks/point/create.js +76 -0
- package/src/actions/marks/point/edit.js +97 -0
- package/src/actions/marks/point/index.js +11 -1
- package/src/actions/marks/point/jitter.js +72 -0
- package/src/actions/marks/point/{actions.js → materialize.js} +183 -172
- package/src/actions/marks/rect/actions.js +3 -0
- package/src/actions/marks/remove.js +13 -2
- package/src/actions/marks/rule/actions.js +29 -4
- package/src/actions/marks/text/actions.js +19 -4
- package/src/actions/marks/text/index.js +7 -1
- package/src/actions/marks/text/layout.js +321 -0
- package/src/actions/primitives/semanticValidation/dataset.js +22 -0
- package/src/actions/primitives/semanticValidation/guide.js +43 -0
- package/src/actions/primitives/semanticValidation/index.js +23 -0
- package/src/actions/primitives/semanticValidation/layer.js +74 -0
- package/src/actions/primitives/semanticValidation/scale.js +74 -0
- package/src/actions/primitives/semanticValidation/shared.js +5 -0
- package/src/actions/primitives/semanticValue.js +1 -204
- package/src/actions/regression/edit.js +247 -88
- package/src/actions/scales/consumers/common.js +18 -0
- package/src/actions/scales/consumers/families.js +14 -3
- package/src/actions/scales/definitions.js +30 -0
- package/src/actions/scales/edit.js +6 -204
- package/src/actions/scales/editPolicy.js +214 -0
- package/src/actions/scales/materialize.js +1 -1
- package/src/actions/scales/patch.js +26 -0
- package/src/actions/selection/actions.js +150 -5
- package/src/actions/selection/index.js +6 -0
- package/src/actions/violinPlots/create.js +198 -0
- package/src/actions/violinPlots/index.js +5 -0
- package/src/core/textMetrics.js +47 -0
- package/src/core/vocabulary.js +14 -2
- package/src/grammar/arcs.js +49 -12
- package/src/grammar/areaSeries.js +41 -5
- package/src/grammar/bin2d.js +333 -0
- package/src/grammar/categoricalDensity.js +192 -0
- package/src/grammar/coordinates.js +1 -1
- package/src/grammar/curveCommands.js +3 -1
- package/src/grammar/density.js +252 -26
- package/src/grammar/facets/dependencies.js +3 -11
- package/src/grammar/facets/index.js +3 -1
- package/src/grammar/gradientProfile.js +213 -0
- package/src/grammar/horizon.js +455 -0
- package/src/grammar/jitter.js +197 -0
- package/src/grammar/lineSeries.js +69 -0
- package/src/grammar/paint.js +88 -0
- package/src/grammar/parallelCoordinates.js +213 -0
- package/src/grammar/pathOrder.js +35 -0
- package/src/grammar/pointShapes.js +24 -0
- package/src/grammar/rules.js +1 -0
- package/src/grammar/scales/appearance.js +21 -0
- package/src/grammar/schemas/concreteGraphic.js +11 -1
- package/src/grammar/schemas/graphicBounds.js +19 -37
- package/src/grammar/schemas/semanticPath.js +17 -2
- package/src/grammar/transforms.js +100 -11
- package/src/grammar/window.js +339 -0
- package/src/layout/labels.js +270 -0
- package/src/layout/text.js +4 -1
- package/src/layout/title.js +10 -20
- package/src/materialization/dataProvenance.js +56 -2
- package/src/materialization/dependencies.js +9 -14
- package/src/materialization/guides/resources.js +74 -24
- package/src/materialization/horizon.js +29 -0
- package/src/materialization/layout.js +15 -0
- package/src/materialization/marks/capabilities.js +21 -1
- package/src/materialization/marks/index.js +3 -0
- package/src/materialization/marks/pathOrder.js +71 -0
- package/src/materialization/marks/policies.js +6 -2
- package/src/materialization/scaleGuideDependencies.js +8 -0
- package/src/materialization/scales/resolve.js +2 -0
- package/src/materialization/selection/items/path.js +27 -1
- package/src/materialization/selection/items/point.js +1 -5
- package/src/materialization/selection/items/rect.js +21 -0
- package/src/materialization/selection/styles.js +20 -9
- package/src/materialization/text.js +12 -4
- package/src/renderers/canvas/fill.js +33 -0
- package/src/renderers/canvas/index.js +7 -0
- package/src/renderers/canvas/path.js +7 -4
- package/src/renderers/canvas/rect.js +9 -3
- package/src/selectors/datasets.js +4 -0
- package/src/theme/defaults.js +4 -0
- package/types/extension.d.ts +6 -0
- package/types/index.d.ts +77 -0
- package/types/program.d.ts +703 -17
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { validateUserId } from "../../../core/identifiers.js";
|
|
2
|
+
import { isPlainObject } from "../../../core/immutable.js";
|
|
3
|
+
import { validateOptionObject } from "../../../core/validation.js";
|
|
4
|
+
import { resolvePalette } from "../../../grammar/palettes.js";
|
|
5
|
+
import {
|
|
6
|
+
normalizeTemporalValue,
|
|
7
|
+
readNominalField
|
|
8
|
+
} from "../../../grammar/scales/index.js";
|
|
9
|
+
import {
|
|
10
|
+
findDataset,
|
|
11
|
+
findSemanticScale,
|
|
12
|
+
hasDataset,
|
|
13
|
+
resolveEligibleLayer
|
|
14
|
+
} from "../../../selectors/index.js";
|
|
15
|
+
|
|
16
|
+
export function findHorizonArea(program, requested) {
|
|
17
|
+
const target = requested === undefined
|
|
18
|
+
? undefined
|
|
19
|
+
: validateUserId(requested, "Horizon target id");
|
|
20
|
+
return resolveEligibleLayer(program, {
|
|
21
|
+
target,
|
|
22
|
+
label: "Horizon area",
|
|
23
|
+
predicate: layer => layer.mark?.type === "area"
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function resolveHorizonSource(program, layer, requested) {
|
|
28
|
+
const id = validateUserId(
|
|
29
|
+
requested ?? layer.data ?? program.context.currentData,
|
|
30
|
+
"Horizon source dataset id"
|
|
31
|
+
);
|
|
32
|
+
if (!hasDataset(program, id)) {
|
|
33
|
+
throw new Error(`Unknown Horizon source dataset "${id}".`);
|
|
34
|
+
}
|
|
35
|
+
return findDataset(program, id);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function fieldOption(value, role) {
|
|
39
|
+
if (typeof value === "string") return { field: value };
|
|
40
|
+
if (!isPlainObject(value)) {
|
|
41
|
+
throw new TypeError(`Horizon ${role} must be a field string or plain object.`);
|
|
42
|
+
}
|
|
43
|
+
validateOptionObject(
|
|
44
|
+
value,
|
|
45
|
+
["field", "fieldType", "scale"],
|
|
46
|
+
`Horizon ${role}`
|
|
47
|
+
);
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function inferredFieldType(rows, field, role) {
|
|
52
|
+
if (role === "y") {
|
|
53
|
+
const valid = rows.filter(row => Number.isFinite(row?.[field]));
|
|
54
|
+
if (valid.length === 0) {
|
|
55
|
+
throw new TypeError(`Horizon y field "${field}" requires a finite value.`);
|
|
56
|
+
}
|
|
57
|
+
return "quantitative";
|
|
58
|
+
}
|
|
59
|
+
if (rows.every(row => Number.isFinite(row?.[field]))) return "quantitative";
|
|
60
|
+
for (const [index, row] of rows.entries()) {
|
|
61
|
+
normalizeTemporalValue(row?.[field], field, index);
|
|
62
|
+
}
|
|
63
|
+
return "temporal";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function compatibleEncoding(encoding, role) {
|
|
67
|
+
return encoding?.field !== undefined && (
|
|
68
|
+
role === "x"
|
|
69
|
+
? ["quantitative", "temporal"].includes(encoding.fieldType)
|
|
70
|
+
: encoding.fieldType === "quantitative"
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function uniqueStoredEncoding(program, dataset, role) {
|
|
75
|
+
const candidates = program.semanticSpec.layers
|
|
76
|
+
.filter(layer => layer.data === dataset.id)
|
|
77
|
+
.map(layer => layer.encoding?.[role])
|
|
78
|
+
.filter(encoding => compatibleEncoding(encoding, role));
|
|
79
|
+
const unique = new Map(candidates.map(encoding => [
|
|
80
|
+
JSON.stringify([
|
|
81
|
+
encoding.field,
|
|
82
|
+
encoding.fieldType,
|
|
83
|
+
encoding.scale,
|
|
84
|
+
encoding.title
|
|
85
|
+
]),
|
|
86
|
+
encoding
|
|
87
|
+
]));
|
|
88
|
+
if (unique.size > 1) {
|
|
89
|
+
throw new Error(`Horizon ${role} inference is ambiguous; provide ${role}.`);
|
|
90
|
+
}
|
|
91
|
+
return [...unique.values()][0];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function resolveHorizonField(program, layer, dataset, requested, role) {
|
|
95
|
+
const existing = compatibleEncoding(layer.encoding?.[role], role)
|
|
96
|
+
? layer.encoding[role]
|
|
97
|
+
: undefined;
|
|
98
|
+
if (requested === undefined) {
|
|
99
|
+
const inferred = existing ?? uniqueStoredEncoding(program, dataset, role);
|
|
100
|
+
if (inferred === undefined) {
|
|
101
|
+
throw new Error(`encodeHorizon requires ${role} or one inferable ${role} encoding.`);
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
field: inferred.field,
|
|
105
|
+
fieldType: inferred.fieldType,
|
|
106
|
+
...(role === "x" && inferred.scale !== undefined
|
|
107
|
+
? { scale: { id: inferred.scale } }
|
|
108
|
+
: {}),
|
|
109
|
+
...(inferred.title === undefined ? {} : { title: inferred.title })
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const option = fieldOption(requested, role);
|
|
113
|
+
if (typeof option.field !== "string" || option.field.length === 0) {
|
|
114
|
+
throw new TypeError(`Horizon ${role} field must be a non-empty string.`);
|
|
115
|
+
}
|
|
116
|
+
const matching = existing?.field === option.field ? existing : undefined;
|
|
117
|
+
if (option.scale !== undefined && !isPlainObject(option.scale)) {
|
|
118
|
+
throw new TypeError(`Horizon ${role} scale must be a plain object.`);
|
|
119
|
+
}
|
|
120
|
+
const fieldType = option.fieldType ?? matching?.fieldType ??
|
|
121
|
+
inferredFieldType(dataset.values, option.field, role);
|
|
122
|
+
if (
|
|
123
|
+
(role === "x" && !["quantitative", "temporal"].includes(fieldType)) ||
|
|
124
|
+
(role === "y" && fieldType !== "quantitative")
|
|
125
|
+
) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`Horizon ${role} fieldType must be ${role === "x" ? "quantitative or temporal" : "quantitative"}.`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
field: option.field,
|
|
132
|
+
fieldType,
|
|
133
|
+
...(option.scale !== undefined
|
|
134
|
+
? { scale: option.scale }
|
|
135
|
+
: role === "x" && matching?.scale !== undefined
|
|
136
|
+
? { scale: { id: matching.scale } }
|
|
137
|
+
: {}),
|
|
138
|
+
...(matching?.title === undefined ? {} : { title: matching.title })
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function resolveHorizonGroupBy(layer, dataset, requested) {
|
|
143
|
+
if (requested === false) {
|
|
144
|
+
throw new Error("encodeHorizon groupBy must be a field string when provided.");
|
|
145
|
+
}
|
|
146
|
+
const value = requested ?? layer.encoding?.group?.field;
|
|
147
|
+
if (value === undefined) return undefined;
|
|
148
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
149
|
+
throw new TypeError("Horizon groupBy must be a non-empty string.");
|
|
150
|
+
}
|
|
151
|
+
readNominalField(dataset.values, value);
|
|
152
|
+
return value;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function horizonOutputFields(target) {
|
|
156
|
+
const prefix = `${target}Horizon`;
|
|
157
|
+
return {
|
|
158
|
+
x: `${prefix}X`, lower: `${prefix}Lower`, upper: `${prefix}Upper`,
|
|
159
|
+
group: `${prefix}Group`, color: `${prefix}Color`, sign: `${prefix}Sign`,
|
|
160
|
+
band: `${prefix}Band`, segment: `${prefix}Segment`
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function resolveHorizonPaletteColors(transform) {
|
|
165
|
+
const domain = [];
|
|
166
|
+
const range = [];
|
|
167
|
+
for (const sign of ["negative", "positive"]) {
|
|
168
|
+
const palette = transform.palette[sign];
|
|
169
|
+
const dense = palette.count === undefined && transform.bands > 1
|
|
170
|
+
? resolvePalette({ ...palette, count: transform.bands * 2 }, transform.bands * 2)
|
|
171
|
+
: resolvePalette(palette, transform.bands);
|
|
172
|
+
const colors = palette.count === undefined && transform.bands > 1
|
|
173
|
+
? Array.from({ length: transform.bands }, (_, index) =>
|
|
174
|
+
index === transform.bands - 1 ? dense.at(-1) : dense[index * 2])
|
|
175
|
+
: dense;
|
|
176
|
+
for (let band = 0; band < transform.bands; band += 1) {
|
|
177
|
+
domain.push(`${sign}:${band}`);
|
|
178
|
+
range.push(colors[band]);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return { domain, range };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function resolveFoldedHorizonScaleOptions(target, requested) {
|
|
185
|
+
if (requested !== undefined && !isPlainObject(requested)) {
|
|
186
|
+
throw new TypeError("Horizon y scale must be a plain object.");
|
|
187
|
+
}
|
|
188
|
+
const options = requested ?? {};
|
|
189
|
+
if (
|
|
190
|
+
options.domain !== undefined &&
|
|
191
|
+
(!Array.isArray(options.domain) ||
|
|
192
|
+
options.domain.length !== 2 ||
|
|
193
|
+
options.domain[0] !== 0 ||
|
|
194
|
+
options.domain[1] !== 1)
|
|
195
|
+
) {
|
|
196
|
+
throw new Error("Horizon y scale domain must be [0, 1].");
|
|
197
|
+
}
|
|
198
|
+
if (options.type !== undefined && options.type !== "linear") {
|
|
199
|
+
throw new Error('Horizon y scale type must be "linear".');
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
...options,
|
|
203
|
+
id: options.id ?? `${target}HorizonAmplitude`,
|
|
204
|
+
type: "linear",
|
|
205
|
+
domain: [0, 1],
|
|
206
|
+
nice: false,
|
|
207
|
+
zero: true
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function inferHorizonTitle(field) {
|
|
212
|
+
const words = field.replaceAll("_", " ");
|
|
213
|
+
return words.length === 0 ? field : words[0].toUpperCase() + words.slice(1);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function validateUnusedHorizonEncodings(layer) {
|
|
217
|
+
const conflicts = ["x2", "y2", "color", "pathOrder"].filter(
|
|
218
|
+
channel => layer.encoding?.[channel] !== undefined
|
|
219
|
+
);
|
|
220
|
+
if (conflicts.length > 0) {
|
|
221
|
+
throw new Error(
|
|
222
|
+
`Horizon area target "${layer.id}" already has unsupported ${conflicts.join(", ")} encoding.`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function findExistingHorizonScale(program, id, label) {
|
|
228
|
+
const scale = findSemanticScale(program, id);
|
|
229
|
+
if (scale === undefined) throw new Error(`${label} scale "${id}" is missing.`);
|
|
230
|
+
return scale;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function resolveEditedHorizonField(
|
|
234
|
+
program,
|
|
235
|
+
layer,
|
|
236
|
+
source,
|
|
237
|
+
transform,
|
|
238
|
+
requested,
|
|
239
|
+
role
|
|
240
|
+
) {
|
|
241
|
+
if (requested === undefined) {
|
|
242
|
+
return {
|
|
243
|
+
...transform[role],
|
|
244
|
+
...(role === "x" ? { title: layer.encoding.x.title } : {})
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
const prior = transform[role];
|
|
248
|
+
return resolveHorizonField(program, {
|
|
249
|
+
...layer,
|
|
250
|
+
encoding: {
|
|
251
|
+
...layer.encoding,
|
|
252
|
+
[role]: {
|
|
253
|
+
field: prior.field,
|
|
254
|
+
fieldType: prior.fieldType,
|
|
255
|
+
scale: layer.encoding[role].scale,
|
|
256
|
+
...(role === "x" && layer.encoding.x.title !== undefined
|
|
257
|
+
? { title: layer.encoding.x.title }
|
|
258
|
+
: {})
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}, source, requested, role);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function resolveEditedHorizonGroupBy(source, previous, requested) {
|
|
265
|
+
if (requested === undefined) return previous;
|
|
266
|
+
if (requested === false) return undefined;
|
|
267
|
+
if (typeof requested !== "string" || requested.length === 0) {
|
|
268
|
+
throw new TypeError("Horizon groupBy must be a field string or false.");
|
|
269
|
+
}
|
|
270
|
+
readNominalField(source.values, requested);
|
|
271
|
+
return requested;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function mergeHorizonPalette(previous, requested) {
|
|
275
|
+
if (requested === undefined) return previous;
|
|
276
|
+
if (!isPlainObject(requested)) {
|
|
277
|
+
throw new TypeError("Horizon palette must be a plain object.");
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
positive: requested.positive ?? previous.positive,
|
|
281
|
+
negative: requested.negative ?? previous.negative
|
|
282
|
+
};
|
|
283
|
+
}
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import { validateUserId } from "../../core/identifiers.js";
|
|
3
|
+
import { validateOptionObject } from "../../core/validation.js";
|
|
4
|
+
import {
|
|
5
|
+
deriveHorizon,
|
|
6
|
+
validateHorizonTransform
|
|
7
|
+
} from "../../grammar/horizon.js";
|
|
8
|
+
import {
|
|
9
|
+
normalizeTemporalValue,
|
|
10
|
+
readQuantitativeField
|
|
11
|
+
} from "../../grammar/scales/index.js";
|
|
12
|
+
import {
|
|
13
|
+
resolveColorScaleDefinition,
|
|
14
|
+
resolvePositionScaleDefinition
|
|
15
|
+
} from "../scales/definitions.js";
|
|
16
|
+
import {
|
|
17
|
+
findDataset,
|
|
18
|
+
hasDataset,
|
|
19
|
+
resolveEligibleLayer
|
|
20
|
+
} from "../../selectors/index.js";
|
|
21
|
+
import { applyMaterializationPlan } from
|
|
22
|
+
"../../materialization/dependencies.js";
|
|
23
|
+
import { planHorizonRematerialization } from
|
|
24
|
+
"../../materialization/horizon.js";
|
|
25
|
+
import { planDerivedDataRevision } from
|
|
26
|
+
"../../materialization/dataProvenance.js";
|
|
27
|
+
import { changedScaleEditPatch } from "../scales/patch.js";
|
|
28
|
+
import {
|
|
29
|
+
findExistingHorizonScale,
|
|
30
|
+
findHorizonArea,
|
|
31
|
+
horizonOutputFields,
|
|
32
|
+
inferHorizonTitle,
|
|
33
|
+
mergeHorizonPalette,
|
|
34
|
+
resolveEditedHorizonField,
|
|
35
|
+
resolveEditedHorizonGroupBy,
|
|
36
|
+
resolveFoldedHorizonScaleOptions,
|
|
37
|
+
resolveHorizonField,
|
|
38
|
+
resolveHorizonGroupBy,
|
|
39
|
+
resolveHorizonPaletteColors,
|
|
40
|
+
resolveHorizonSource,
|
|
41
|
+
validateUnusedHorizonEncodings
|
|
42
|
+
} from "./horizon/resolve.js";
|
|
43
|
+
|
|
44
|
+
const OPTIONS = Object.freeze([
|
|
45
|
+
"target", "source", "x", "y", "groupBy", "bands", "baseline", "extent",
|
|
46
|
+
"resolve", "missing", "overflow", "palette"
|
|
47
|
+
]);
|
|
48
|
+
const EDIT_OPTIONS = OPTIONS;
|
|
49
|
+
const EDITABLE = Object.freeze(EDIT_OPTIONS.filter(option => option !== "target"));
|
|
50
|
+
|
|
51
|
+
function clearOwnedEncodings(program, layer) {
|
|
52
|
+
let next = program;
|
|
53
|
+
for (const channel of ["x", "y", "group"]) {
|
|
54
|
+
if (layer.encoding?.[channel] === undefined) continue;
|
|
55
|
+
next = next.editSemantic({
|
|
56
|
+
property: `layer[${layer.id}].encoding.${channel}`,
|
|
57
|
+
remove: true
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return next;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function applyHorizonEncoding(program, {
|
|
64
|
+
layer,
|
|
65
|
+
transform,
|
|
66
|
+
xScale,
|
|
67
|
+
yScale,
|
|
68
|
+
colorScale
|
|
69
|
+
}) {
|
|
70
|
+
let next = clearOwnedEncodings(program, layer)
|
|
71
|
+
.encodeX({
|
|
72
|
+
target: layer.id,
|
|
73
|
+
field: transform.as.x,
|
|
74
|
+
fieldType: transform.x.fieldType,
|
|
75
|
+
scale: xScale
|
|
76
|
+
})
|
|
77
|
+
.editSemantic({
|
|
78
|
+
property: `layer[${layer.id}].encoding.x.title`,
|
|
79
|
+
value: layer.encoding?.x?.title ?? inferHorizonTitle(transform.x.field)
|
|
80
|
+
})
|
|
81
|
+
.encodeY({
|
|
82
|
+
target: layer.id,
|
|
83
|
+
field: transform.as.lower,
|
|
84
|
+
fieldType: "quantitative",
|
|
85
|
+
scale: yScale
|
|
86
|
+
})
|
|
87
|
+
.encodeGroup({ target: layer.id, field: transform.as.group })
|
|
88
|
+
.encodeY2({
|
|
89
|
+
target: layer.id,
|
|
90
|
+
field: transform.as.upper,
|
|
91
|
+
fieldType: "quantitative"
|
|
92
|
+
})
|
|
93
|
+
.encodeColor({
|
|
94
|
+
target: layer.id,
|
|
95
|
+
field: transform.as.color,
|
|
96
|
+
fieldType: "nominal",
|
|
97
|
+
scale: colorScale
|
|
98
|
+
})
|
|
99
|
+
.editAreaMark({ target: layer.id, opacity: 1 });
|
|
100
|
+
return next;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function applyScaleDefinition(program, current, definition) {
|
|
104
|
+
const patch = changedScaleEditPatch(current, definition);
|
|
105
|
+
return patch === undefined ? program : program.editScale(patch);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const editHorizon = action(
|
|
109
|
+
{
|
|
110
|
+
op: "editHorizon",
|
|
111
|
+
description: "Revise one Horizon transform and rematerialize its consumers."
|
|
112
|
+
},
|
|
113
|
+
function (args = {}) {
|
|
114
|
+
validateOptionObject(args, EDIT_OPTIONS, "editHorizon");
|
|
115
|
+
if (!EDITABLE.some(option => Object.hasOwn(args, option))) {
|
|
116
|
+
throw new Error("editHorizon requires at least one Horizon option.");
|
|
117
|
+
}
|
|
118
|
+
const layer = resolveEligibleLayer(this, {
|
|
119
|
+
target: args.target,
|
|
120
|
+
label: "Horizon area",
|
|
121
|
+
predicate: candidate => {
|
|
122
|
+
const dataset = findDataset(this, candidate.data);
|
|
123
|
+
return candidate.mark?.type === "area" &&
|
|
124
|
+
dataset?.transform?.length === 1 &&
|
|
125
|
+
dataset.transform[0].type === "horizon";
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
const previous = findDataset(this, layer.data);
|
|
129
|
+
const prior = previous.transform[0];
|
|
130
|
+
const sourceId = validateUserId(
|
|
131
|
+
args.source ?? previous.source,
|
|
132
|
+
"Horizon source dataset id"
|
|
133
|
+
);
|
|
134
|
+
const source = findDataset(this, sourceId);
|
|
135
|
+
if (source === undefined) {
|
|
136
|
+
throw new Error(`Unknown Horizon source dataset "${sourceId}".`);
|
|
137
|
+
}
|
|
138
|
+
const x = resolveEditedHorizonField(this, layer, source, prior, args.x, "x");
|
|
139
|
+
const y = resolveEditedHorizonField(this, layer, source, prior, args.y, "y");
|
|
140
|
+
const groupBy = resolveEditedHorizonGroupBy(
|
|
141
|
+
source,
|
|
142
|
+
prior.groupBy,
|
|
143
|
+
args.groupBy
|
|
144
|
+
);
|
|
145
|
+
const candidate = validateHorizonTransform({
|
|
146
|
+
type: "horizon",
|
|
147
|
+
x: { field: x.field, fieldType: x.fieldType },
|
|
148
|
+
y: { field: y.field, fieldType: y.fieldType },
|
|
149
|
+
...(groupBy === undefined ? {} : { groupBy }),
|
|
150
|
+
bands: args.bands ?? prior.bands,
|
|
151
|
+
baseline: args.baseline ?? prior.baseline,
|
|
152
|
+
extent: args.extent ?? prior.extent,
|
|
153
|
+
resolve: args.resolve ?? prior.resolve,
|
|
154
|
+
missing: args.missing ?? prior.missing,
|
|
155
|
+
overflow: args.overflow ?? prior.overflow,
|
|
156
|
+
palette: mergeHorizonPalette(prior.palette, args.palette),
|
|
157
|
+
as: prior.as
|
|
158
|
+
});
|
|
159
|
+
const preflight = deriveHorizon(source.values, candidate);
|
|
160
|
+
const revision = planDerivedDataRevision(this, {
|
|
161
|
+
owner: layer.id,
|
|
162
|
+
role: "HorizonData",
|
|
163
|
+
previous: previous.id,
|
|
164
|
+
consumers: [layer.id]
|
|
165
|
+
});
|
|
166
|
+
let next = this
|
|
167
|
+
.createHorizonData({
|
|
168
|
+
id: revision.id,
|
|
169
|
+
source: source.id,
|
|
170
|
+
x: candidate.x,
|
|
171
|
+
y: candidate.y,
|
|
172
|
+
...(groupBy === undefined ? {} : { groupBy }),
|
|
173
|
+
bands: candidate.bands,
|
|
174
|
+
baseline: candidate.baseline,
|
|
175
|
+
extent: candidate.extent,
|
|
176
|
+
resolve: candidate.resolve,
|
|
177
|
+
missing: candidate.missing,
|
|
178
|
+
overflow: candidate.overflow,
|
|
179
|
+
palette: candidate.palette,
|
|
180
|
+
as: candidate.as
|
|
181
|
+
})
|
|
182
|
+
.rebindLayerData(revision.rebinds[0])
|
|
183
|
+
.releaseDerivedData(revision.release);
|
|
184
|
+
|
|
185
|
+
if (layer.encoding.x.fieldType !== x.fieldType) {
|
|
186
|
+
next = next.editSemantic({
|
|
187
|
+
property: `layer[${layer.id}].encoding.x.fieldType`,
|
|
188
|
+
value: x.fieldType
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
if (Object.hasOwn(args, "x")) {
|
|
192
|
+
next = next.editSemantic({
|
|
193
|
+
property: `layer[${layer.id}].encoding.x.title`,
|
|
194
|
+
value: inferHorizonTitle(x.field)
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const currentX = findExistingHorizonScale(
|
|
199
|
+
next,
|
|
200
|
+
layer.encoding.x.scale,
|
|
201
|
+
"Horizon x"
|
|
202
|
+
);
|
|
203
|
+
const requestedXScale = x.scale ?? {};
|
|
204
|
+
if (requestedXScale.id !== undefined && requestedXScale.id !== currentX.id) {
|
|
205
|
+
throw new Error("editHorizon x scale cannot change its id.");
|
|
206
|
+
}
|
|
207
|
+
const xTypeChanged = prior.x.fieldType !== x.fieldType;
|
|
208
|
+
const emptyXDomain = preflight.values.length === 0 &&
|
|
209
|
+
requestedXScale.domain === undefined &&
|
|
210
|
+
currentX.domain === "auto"
|
|
211
|
+
? source.values.map((row, index) => x.fieldType === "temporal"
|
|
212
|
+
? normalizeTemporalValue(row[x.field], x.field, index)
|
|
213
|
+
: row[x.field])
|
|
214
|
+
: undefined;
|
|
215
|
+
const nextX = resolvePositionScaleDefinition(next, "x", x.fieldType, {
|
|
216
|
+
...requestedXScale,
|
|
217
|
+
id: currentX.id,
|
|
218
|
+
...(emptyXDomain === undefined
|
|
219
|
+
? {}
|
|
220
|
+
: { domain: [Math.min(...emptyXDomain), Math.max(...emptyXDomain)] }),
|
|
221
|
+
...(xTypeChanged && requestedXScale.type === undefined
|
|
222
|
+
? { type: x.fieldType === "temporal" ? "time" : "linear" }
|
|
223
|
+
: {})
|
|
224
|
+
});
|
|
225
|
+
next = applyScaleDefinition(next, currentX, nextX);
|
|
226
|
+
|
|
227
|
+
const currentY = findExistingHorizonScale(
|
|
228
|
+
next,
|
|
229
|
+
layer.encoding.y.scale,
|
|
230
|
+
"Horizon y"
|
|
231
|
+
);
|
|
232
|
+
const requestedYScale = y.scale ?? {};
|
|
233
|
+
if (requestedYScale.id !== undefined && requestedYScale.id !== currentY.id) {
|
|
234
|
+
throw new Error("editHorizon y scale cannot change its id.");
|
|
235
|
+
}
|
|
236
|
+
const nextY = resolveFoldedHorizonScaleOptions(layer.id, {
|
|
237
|
+
...currentY,
|
|
238
|
+
...requestedYScale,
|
|
239
|
+
id: currentY.id
|
|
240
|
+
});
|
|
241
|
+
next = applyScaleDefinition(next, currentY, nextY);
|
|
242
|
+
|
|
243
|
+
const colors = resolveHorizonPaletteColors(candidate);
|
|
244
|
+
const currentColor = findExistingHorizonScale(
|
|
245
|
+
next,
|
|
246
|
+
layer.encoding.color.scale,
|
|
247
|
+
"Horizon color"
|
|
248
|
+
);
|
|
249
|
+
const nextColor = resolveColorScaleDefinition(next, {
|
|
250
|
+
id: currentColor.id,
|
|
251
|
+
type: "ordinal",
|
|
252
|
+
domain: colors.domain,
|
|
253
|
+
range: colors.range
|
|
254
|
+
});
|
|
255
|
+
next = applyScaleDefinition(next, currentColor, nextColor);
|
|
256
|
+
|
|
257
|
+
if (preflight.values.length === 0) {
|
|
258
|
+
return next.editGraphics({
|
|
259
|
+
target: layer.id,
|
|
260
|
+
property: "length",
|
|
261
|
+
value: 0
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
return applyMaterializationPlan(
|
|
265
|
+
next,
|
|
266
|
+
planHorizonRematerialization(next, layer.id)
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const encodeHorizon = action(
|
|
272
|
+
{
|
|
273
|
+
op: "encodeHorizon",
|
|
274
|
+
description: "Derive and encode one folded Horizon area."
|
|
275
|
+
},
|
|
276
|
+
function (args = {}) {
|
|
277
|
+
validateOptionObject(args, OPTIONS, "encodeHorizon");
|
|
278
|
+
const layer = findHorizonArea(this, args.target);
|
|
279
|
+
validateUnusedHorizonEncodings(layer);
|
|
280
|
+
const source = resolveHorizonSource(this, layer, args.source);
|
|
281
|
+
const x = resolveHorizonField(this, layer, source, args.x, "x");
|
|
282
|
+
const y = resolveHorizonField(this, layer, source, args.y, "y");
|
|
283
|
+
const groupBy = resolveHorizonGroupBy(layer, source, args.groupBy);
|
|
284
|
+
const as = horizonOutputFields(layer.id);
|
|
285
|
+
const transform = validateHorizonTransform({
|
|
286
|
+
type: "horizon",
|
|
287
|
+
x: { field: x.field, fieldType: x.fieldType },
|
|
288
|
+
y: { field: y.field, fieldType: y.fieldType },
|
|
289
|
+
...(groupBy === undefined ? {} : { groupBy }),
|
|
290
|
+
bands: args.bands ?? 3,
|
|
291
|
+
baseline: args.baseline ?? 0,
|
|
292
|
+
extent: args.extent ?? "auto",
|
|
293
|
+
resolve: args.resolve ?? "shared",
|
|
294
|
+
missing: args.missing ?? "break",
|
|
295
|
+
overflow: args.overflow ?? "clip",
|
|
296
|
+
palette: args.palette ?? {},
|
|
297
|
+
as
|
|
298
|
+
});
|
|
299
|
+
const preflight = deriveHorizon(source.values, transform);
|
|
300
|
+
const id = `${layer.id}HorizonData`;
|
|
301
|
+
if (hasDataset(this, id)) {
|
|
302
|
+
throw new Error(`Dataset "${id}" already exists.`);
|
|
303
|
+
}
|
|
304
|
+
const xScale = {
|
|
305
|
+
...(x.scale ?? {}),
|
|
306
|
+
...(preflight.values.length === 0 && x.scale?.domain === undefined
|
|
307
|
+
? {
|
|
308
|
+
domain: source.values.map((row, index) =>
|
|
309
|
+
x.fieldType === "temporal"
|
|
310
|
+
? normalizeTemporalValue(row[x.field], x.field, index)
|
|
311
|
+
: row[x.field]
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
: {})
|
|
315
|
+
};
|
|
316
|
+
if (Array.isArray(xScale.domain) && xScale.domain.length > 2) {
|
|
317
|
+
xScale.domain = [Math.min(...xScale.domain), Math.max(...xScale.domain)];
|
|
318
|
+
}
|
|
319
|
+
const yScale = resolveFoldedHorizonScaleOptions(layer.id, y.scale);
|
|
320
|
+
const colors = resolveHorizonPaletteColors(transform);
|
|
321
|
+
const colorScale = {
|
|
322
|
+
id: `${layer.id}HorizonColor`,
|
|
323
|
+
type: "ordinal",
|
|
324
|
+
domain: colors.domain,
|
|
325
|
+
range: colors.range
|
|
326
|
+
};
|
|
327
|
+
resolvePositionScaleDefinition(this, "x", x.fieldType, xScale);
|
|
328
|
+
resolvePositionScaleDefinition(this, "y", "quantitative", yScale);
|
|
329
|
+
resolveColorScaleDefinition(this, colorScale);
|
|
330
|
+
if (x.fieldType === "quantitative") readQuantitativeField(source.values, x.field);
|
|
331
|
+
|
|
332
|
+
let next = this
|
|
333
|
+
.createHorizonData({
|
|
334
|
+
id,
|
|
335
|
+
source: source.id,
|
|
336
|
+
x: transform.x,
|
|
337
|
+
y: transform.y,
|
|
338
|
+
...(groupBy === undefined ? {} : { groupBy }),
|
|
339
|
+
bands: transform.bands,
|
|
340
|
+
baseline: transform.baseline,
|
|
341
|
+
extent: transform.extent,
|
|
342
|
+
resolve: transform.resolve,
|
|
343
|
+
missing: transform.missing,
|
|
344
|
+
overflow: transform.overflow,
|
|
345
|
+
palette: transform.palette,
|
|
346
|
+
as
|
|
347
|
+
})
|
|
348
|
+
.rebindLayerData({ id: layer.id, data: id });
|
|
349
|
+
const stored = findDataset(next, id).transform[0];
|
|
350
|
+
next = applyHorizonEncoding(next, {
|
|
351
|
+
layer,
|
|
352
|
+
transform: stored,
|
|
353
|
+
xScale,
|
|
354
|
+
yScale,
|
|
355
|
+
colorScale
|
|
356
|
+
});
|
|
357
|
+
return next;
|
|
358
|
+
}
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
export function registerHorizonEncodingAction(ProgramClass) {
|
|
362
|
+
ProgramClass.prototype.encodeHorizon = encodeHorizon;
|
|
363
|
+
ProgramClass.prototype.editHorizon = editHorizon;
|
|
364
|
+
}
|
|
@@ -3,23 +3,31 @@ import { registerBarWidthEncodingAction } from "./barWidth.js";
|
|
|
3
3
|
import { registerColorEncodingAction } from "./color/index.js";
|
|
4
4
|
import { registerDensityEncodingAction } from "./density.js";
|
|
5
5
|
import { registerHistogramEncodingAction } from "./histogram.js";
|
|
6
|
+
import { registerHorizonEncodingAction } from "./horizon.js";
|
|
6
7
|
import { registerOffsetEncodingAction } from "./offset.js";
|
|
8
|
+
import { registerPathOrderEncodingActions } from "./pathOrder.js";
|
|
7
9
|
import { registerPositionEncodingActions } from "./position/index.js";
|
|
8
10
|
import { registerRangedEncodingActions } from "./ranged.js";
|
|
9
11
|
import { registerStrokeDashEncodingActions } from "./strokeDash.js";
|
|
10
12
|
import { registerRuleAppearanceEncodingActions } from "./ruleAppearance.js";
|
|
11
13
|
import { registerTextEncodingAction } from "./text.js";
|
|
14
|
+
import { registerParallelEncodingAction } from "./parallel.js";
|
|
15
|
+
import { registerEncodingRemovalAction } from "./remove.js";
|
|
12
16
|
|
|
13
17
|
export function registerEncodingActions(ProgramClass) {
|
|
14
18
|
registerPositionEncodingActions(ProgramClass);
|
|
15
19
|
registerRangedEncodingActions(ProgramClass);
|
|
16
20
|
registerOffsetEncodingAction(ProgramClass);
|
|
21
|
+
registerPathOrderEncodingActions(ProgramClass);
|
|
17
22
|
registerBarWidthEncodingAction(ProgramClass);
|
|
18
23
|
registerHistogramEncodingAction(ProgramClass);
|
|
24
|
+
registerHorizonEncodingAction(ProgramClass);
|
|
19
25
|
registerDensityEncodingAction(ProgramClass);
|
|
20
26
|
registerColorEncodingAction(ProgramClass);
|
|
21
27
|
registerStrokeDashEncodingActions(ProgramClass);
|
|
22
28
|
registerAppearanceEncodingAction(ProgramClass);
|
|
23
29
|
registerRuleAppearanceEncodingActions(ProgramClass);
|
|
24
30
|
registerTextEncodingAction(ProgramClass);
|
|
31
|
+
registerParallelEncodingAction(ProgramClass);
|
|
32
|
+
registerEncodingRemovalAction(ProgramClass);
|
|
25
33
|
}
|