ggaction 0.0.4 → 0.0.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/CHANGELOG.md +30 -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 +31 -35
- package/src/actions/boxPlots/materialize.js +3 -0
- package/src/actions/boxPlots/options.js +8 -1
- 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/coordinates/parallel.js +50 -0
- package/src/actions/data/bin2d.js +137 -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 +29 -2
- package/src/actions/data/window.js +50 -0
- 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 +193 -88
- package/src/actions/encodings/horizon/resolve.js +283 -0
- package/src/actions/encodings/horizon.js +364 -0
- package/src/actions/encodings/index.js +6 -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/ruleAppearance.js +88 -10
- package/src/actions/facets/actions.js +1 -2
- 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 +153 -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/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 +48 -11
- package/src/actions/guides/legends/edit.js +5 -0
- package/src/actions/guides/legends/index.js +2 -0
- package/src/actions/guides/legends/remove.js +4 -11
- package/src/actions/guides/legends/strokeWidth.js +170 -0
- package/src/actions/index.js +6 -0
- 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 +85 -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} +175 -170
- package/src/actions/marks/rect/actions.js +3 -0
- package/src/actions/marks/remove.js +13 -2
- package/src/actions/marks/rule/actions.js +25 -3
- 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 +19 -21
- 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 +28 -3
- 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 +7 -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 +76 -0
- package/types/program.d.ts +632 -10
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { action } from "../../core/action.js";
|
|
2
2
|
import { validateUserId } from "../../core/identifiers.js";
|
|
3
|
-
import { isPlainObject } from "../../core/immutable.js";
|
|
4
3
|
import {
|
|
5
4
|
findDataset,
|
|
6
5
|
findLayer,
|
|
@@ -11,15 +10,27 @@ import { applyMaterializationPlan } from
|
|
|
11
10
|
"../../materialization/dependencies.js";
|
|
12
11
|
import { planDensityRematerialization } from
|
|
13
12
|
"../../materialization/density.js";
|
|
13
|
+
import { planDerivedDataRevision } from
|
|
14
|
+
"../../materialization/dataProvenance.js";
|
|
14
15
|
import { validateOptions } from "./shared.js";
|
|
16
|
+
import { normalizeDensityPlacement } from "../../grammar/density.js";
|
|
17
|
+
import { scaleEditPatch } from "../scales/patch.js";
|
|
18
|
+
import {
|
|
19
|
+
requireDensityField,
|
|
20
|
+
resolveDensityCategoryScaleOptions,
|
|
21
|
+
resolveDensityPositionDefinition,
|
|
22
|
+
resolveDensityScaleOptions,
|
|
23
|
+
resolveDensityTransitionScaleDefinitions
|
|
24
|
+
} from "./density/resolve.js";
|
|
15
25
|
|
|
16
26
|
const OPTIONS = Object.freeze([
|
|
17
27
|
"field", "target", "source", "groupBy", "bandwidth", "extent", "steps",
|
|
18
28
|
"kernel", "normalization", "as", "densityChannel", "coordinate",
|
|
19
|
-
"valueScale", "densityScale"
|
|
29
|
+
"valueScale", "densityScale", "placement"
|
|
20
30
|
]);
|
|
21
31
|
const EDIT_OPTIONS = Object.freeze([
|
|
22
|
-
"target", "bandwidth", "extent", "steps", "kernel", "normalization"
|
|
32
|
+
"target", "bandwidth", "extent", "steps", "kernel", "normalization",
|
|
33
|
+
"placement"
|
|
23
34
|
]);
|
|
24
35
|
const EDITABLE = Object.freeze(EDIT_OPTIONS.filter(option => option !== "target"));
|
|
25
36
|
|
|
@@ -42,32 +53,35 @@ function findArea(program, requested) {
|
|
|
42
53
|
throw new Error("encodeDensity target is ambiguous; provide target.");
|
|
43
54
|
}
|
|
44
55
|
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
function applyDensityPosition(program, layerId, definition, groupBy) {
|
|
57
|
+
const withCoordinate = value => ({
|
|
58
|
+
target: layerId,
|
|
59
|
+
field: value.field,
|
|
60
|
+
fieldType: value.fieldType,
|
|
61
|
+
scale: value.scale,
|
|
62
|
+
...(value.coordinate === undefined ? {} : { coordinate: value.coordinate })
|
|
63
|
+
});
|
|
64
|
+
let next = program
|
|
65
|
+
.encodeX(withCoordinate(definition.x))
|
|
66
|
+
.encodeY(withCoordinate(definition.y));
|
|
67
|
+
if (groupBy !== undefined) {
|
|
68
|
+
next = next.encodeGroup({ target: layerId, field: groupBy });
|
|
55
69
|
}
|
|
56
|
-
return
|
|
70
|
+
return next;
|
|
57
71
|
}
|
|
58
72
|
|
|
59
73
|
const encodeDensity = action(
|
|
60
74
|
{
|
|
61
75
|
op: "encodeDensity",
|
|
62
|
-
description: "Derive and encode a baseline
|
|
76
|
+
description: "Derive and encode a baseline or categorical kernel-density area."
|
|
63
77
|
},
|
|
64
78
|
function (args = {}) {
|
|
65
79
|
validateOptions(args, OPTIONS, "encodeDensity");
|
|
66
80
|
const layer = findArea(this, args.target);
|
|
67
|
-
const field =
|
|
81
|
+
const field = requireDensityField(args.field, "Density field");
|
|
68
82
|
const groupBy = args.groupBy === undefined
|
|
69
83
|
? undefined
|
|
70
|
-
:
|
|
84
|
+
: requireDensityField(args.groupBy, "Density groupBy");
|
|
71
85
|
const source = validateUserId(
|
|
72
86
|
args.source ?? layer.data,
|
|
73
87
|
"Density source dataset id"
|
|
@@ -82,7 +96,8 @@ const encodeDensity = action(
|
|
|
82
96
|
`Density area target "${layer.id}" already has positional or group encodings.`
|
|
83
97
|
);
|
|
84
98
|
}
|
|
85
|
-
const
|
|
99
|
+
const categoryPlacement = args.placement?.type === "category";
|
|
100
|
+
const densityChannel = args.densityChannel ?? (categoryPlacement ? "x" : "y");
|
|
86
101
|
if (!["x", "y"].includes(densityChannel)) {
|
|
87
102
|
throw new Error(`Unsupported densityChannel "${densityChannel}".`);
|
|
88
103
|
}
|
|
@@ -94,56 +109,67 @@ const encodeDensity = action(
|
|
|
94
109
|
if (hasDataset(this, derivedId)) {
|
|
95
110
|
throw new Error(`Dataset "${derivedId}" already exists.`);
|
|
96
111
|
}
|
|
97
|
-
const valueScale =
|
|
112
|
+
const valueScale = resolveDensityScaleOptions(
|
|
98
113
|
args.valueScale,
|
|
99
114
|
{ nice: false, zero: false },
|
|
100
115
|
"Density valueScale"
|
|
101
116
|
);
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
117
|
+
const placement = args.placement === undefined
|
|
118
|
+
? undefined
|
|
119
|
+
: normalizeDensityPlacement(args.placement, {
|
|
120
|
+
densityChannel,
|
|
121
|
+
groupBy,
|
|
122
|
+
categoryField: groupBy ?? `${layer.id}DensityCategory`
|
|
123
|
+
});
|
|
124
|
+
if (placement !== undefined && args.densityScale !== undefined) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
"Density category placement cannot be combined with densityScale."
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
const densityScale = placement === undefined
|
|
130
|
+
? resolveDensityScaleOptions(
|
|
131
|
+
args.densityScale,
|
|
132
|
+
{ nice: true, zero: true },
|
|
133
|
+
"Density densityScale"
|
|
134
|
+
)
|
|
135
|
+
: undefined;
|
|
136
|
+
const placementScale = placement === undefined
|
|
137
|
+
? undefined
|
|
138
|
+
: resolveDensityCategoryScaleOptions(args.placement.scale);
|
|
112
139
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
140
|
+
const dataArgs = {
|
|
141
|
+
id: derivedId,
|
|
142
|
+
source,
|
|
143
|
+
field,
|
|
144
|
+
...(groupBy === undefined ? {} : { groupBy }),
|
|
145
|
+
...(args.bandwidth === undefined ? {} : { bandwidth: args.bandwidth }),
|
|
146
|
+
...(args.extent === undefined ? {} : { extent: args.extent }),
|
|
147
|
+
...(args.steps === undefined ? {} : { steps: args.steps }),
|
|
148
|
+
...(args.kernel === undefined ? {} : { kernel: args.kernel }),
|
|
149
|
+
...(args.normalization === undefined
|
|
150
|
+
? {}
|
|
151
|
+
: { normalization: args.normalization }),
|
|
152
|
+
...(args.as === undefined ? {} : { as: args.as }),
|
|
153
|
+
...(placement === undefined ? {} : { placement })
|
|
154
|
+
};
|
|
155
|
+
let next = placement === undefined
|
|
156
|
+
? this.createDensityData(dataArgs)
|
|
157
|
+
: this.createCategoricalDensityData(dataArgs);
|
|
158
|
+
next = next
|
|
128
159
|
.editSemantic({
|
|
129
160
|
property: `layer[${layer.id}].data`,
|
|
130
161
|
value: derivedId
|
|
131
|
-
})
|
|
132
|
-
.encodeX({
|
|
133
|
-
target: layer.id,
|
|
134
|
-
field: xField,
|
|
135
|
-
...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }),
|
|
136
|
-
scale: xScale
|
|
137
|
-
})
|
|
138
|
-
.encodeY({
|
|
139
|
-
target: layer.id,
|
|
140
|
-
field: yField,
|
|
141
|
-
...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }),
|
|
142
|
-
scale: yScale
|
|
143
162
|
});
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
163
|
+
next = applyDensityPosition(next, layer.id, resolveDensityPositionDefinition({
|
|
164
|
+
layer: { densityChannel },
|
|
165
|
+
output,
|
|
166
|
+
groupBy,
|
|
167
|
+
placement,
|
|
168
|
+
coordinate: args.coordinate,
|
|
169
|
+
valueScale,
|
|
170
|
+
densityScale,
|
|
171
|
+
placementScale
|
|
172
|
+
}), groupBy);
|
|
147
173
|
return next.rematerializeAreaMark({ id: layer.id });
|
|
148
174
|
}
|
|
149
175
|
);
|
|
@@ -164,14 +190,6 @@ function findDensityArea(program, requested) {
|
|
|
164
190
|
});
|
|
165
191
|
}
|
|
166
192
|
|
|
167
|
-
function nextDensityRevisionId(program, target) {
|
|
168
|
-
let revision = 1;
|
|
169
|
-
while (hasDataset(program, `${target}DensityDataRevision${revision}`)) {
|
|
170
|
-
revision += 1;
|
|
171
|
-
}
|
|
172
|
-
return `${target}DensityDataRevision${revision}`;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
193
|
const editDensity = action(
|
|
176
194
|
{
|
|
177
195
|
op: "editDensity",
|
|
@@ -185,31 +203,118 @@ const editDensity = action(
|
|
|
185
203
|
const layer = findDensityArea(this, args.target);
|
|
186
204
|
const previous = findDataset(this, layer.data);
|
|
187
205
|
const transform = previous.transform[0];
|
|
188
|
-
const
|
|
206
|
+
const requestedPlacement = Object.hasOwn(args, "placement")
|
|
207
|
+
? normalizeDensityPlacement(args.placement, {
|
|
208
|
+
densityChannel: transform.placement?.channel ?? "x",
|
|
209
|
+
groupBy: transform.groupBy,
|
|
210
|
+
categoryField: transform.groupBy ??
|
|
211
|
+
transform.placement?.categoryField ??
|
|
212
|
+
`${layer.id}DensityCategory`
|
|
213
|
+
})
|
|
214
|
+
: transform.placement;
|
|
215
|
+
const colorField = layer.encoding?.color?.field;
|
|
216
|
+
const availableColorFields = [
|
|
217
|
+
transform.groupBy,
|
|
218
|
+
requestedPlacement?.split?.field
|
|
219
|
+
].filter(Boolean);
|
|
220
|
+
if (colorField !== undefined && !availableColorFields.includes(colorField)) {
|
|
221
|
+
throw new Error(
|
|
222
|
+
`editDensity placement would remove color field "${colorField}" from the density series.`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
const revision = planDerivedDataRevision(this, {
|
|
226
|
+
owner: layer.id,
|
|
227
|
+
role: "DensityData",
|
|
228
|
+
previous: previous.id,
|
|
229
|
+
consumers: [layer.id]
|
|
230
|
+
});
|
|
189
231
|
const option = property => Object.hasOwn(args, property)
|
|
190
232
|
? args[property]
|
|
191
233
|
: transform[property];
|
|
192
234
|
|
|
193
|
-
|
|
194
|
-
.
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
235
|
+
const dataArgs = {
|
|
236
|
+
id: revision.id,
|
|
237
|
+
source: previous.source,
|
|
238
|
+
field: transform.field,
|
|
239
|
+
...(transform.groupBy === undefined
|
|
240
|
+
? {}
|
|
241
|
+
: { groupBy: transform.groupBy }),
|
|
242
|
+
bandwidth: option("bandwidth"),
|
|
243
|
+
extent: option("extent"),
|
|
244
|
+
steps: option("steps"),
|
|
245
|
+
kernel: option("kernel") ?? "gaussian",
|
|
246
|
+
normalization: option("normalization") ?? "unit",
|
|
247
|
+
as: transform.as,
|
|
248
|
+
...(requestedPlacement === undefined
|
|
249
|
+
? {}
|
|
250
|
+
: { placement: requestedPlacement })
|
|
251
|
+
};
|
|
252
|
+
let next = requestedPlacement === undefined
|
|
253
|
+
? this.createDensityData(dataArgs)
|
|
254
|
+
: this.createCategoricalDensityData(dataArgs);
|
|
255
|
+
next = next.rebindLayerData(revision.rebinds[0])
|
|
256
|
+
.releaseDerivedData(revision.release);
|
|
257
|
+
|
|
258
|
+
const changesPlacementMode = Object.hasOwn(args, "placement") &&
|
|
259
|
+
(transform.placement === undefined) !== (requestedPlacement === undefined);
|
|
260
|
+
if (changesPlacementMode) {
|
|
261
|
+
const xScaleId = layer.encoding?.x?.scale;
|
|
262
|
+
const yScaleId = layer.encoding?.y?.scale;
|
|
263
|
+
const scaleDefinitions = resolveDensityTransitionScaleDefinitions(
|
|
264
|
+
this,
|
|
265
|
+
layer,
|
|
266
|
+
transform,
|
|
267
|
+
requestedPlacement,
|
|
268
|
+
args.placement
|
|
269
|
+
);
|
|
270
|
+
next = next
|
|
271
|
+
.editSemantic({ property: `layer[${layer.id}].encoding.x`, remove: true })
|
|
272
|
+
.editSemantic({ property: `layer[${layer.id}].encoding.y`, remove: true });
|
|
273
|
+
next = next
|
|
274
|
+
.editScale(scaleEditPatch(xScaleId, scaleDefinitions.x))
|
|
275
|
+
.editScale(scaleEditPatch(yScaleId, scaleDefinitions.y));
|
|
276
|
+
const densityChannel = requestedPlacement?.channel ?? (
|
|
277
|
+
layer.encoding.x.field === transform.as[1] ? "x" : "y"
|
|
278
|
+
);
|
|
279
|
+
const definition = resolveDensityPositionDefinition({
|
|
280
|
+
layer: { densityChannel },
|
|
281
|
+
output: transform.as,
|
|
282
|
+
groupBy: transform.groupBy,
|
|
283
|
+
placement: requestedPlacement,
|
|
284
|
+
coordinate: layer.coordinate,
|
|
285
|
+
valueScale: {
|
|
286
|
+
id: requestedPlacement?.channel === "x" ? yScaleId : xScaleId,
|
|
287
|
+
...scaleDefinitions[requestedPlacement?.channel === "x" ? "y" : "x"]
|
|
288
|
+
},
|
|
289
|
+
densityScale: requestedPlacement === undefined
|
|
290
|
+
? {
|
|
291
|
+
id: densityChannel === "x" ? xScaleId : yScaleId,
|
|
292
|
+
...scaleDefinitions[densityChannel]
|
|
293
|
+
}
|
|
294
|
+
: undefined,
|
|
295
|
+
placementScale: requestedPlacement === undefined
|
|
296
|
+
? undefined
|
|
297
|
+
: {
|
|
298
|
+
id: requestedPlacement.channel === "x" ? xScaleId : yScaleId,
|
|
299
|
+
...scaleDefinitions[requestedPlacement.channel]
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
next = applyDensityPosition(next, layer.id, definition);
|
|
303
|
+
} else if (
|
|
304
|
+
requestedPlacement !== undefined &&
|
|
305
|
+
Object.hasOwn(args.placement ?? {}, "scale")
|
|
306
|
+
) {
|
|
307
|
+
const channel = requestedPlacement.channel;
|
|
308
|
+
const scaleId = layer.encoding[channel].scale;
|
|
309
|
+
const requestedScale = resolveDensityCategoryScaleOptions(
|
|
310
|
+
args.placement.scale,
|
|
311
|
+
scaleId
|
|
312
|
+
);
|
|
313
|
+
if (requestedScale.id !== scaleId) {
|
|
314
|
+
throw new Error("editDensity placement scale cannot change its id.");
|
|
315
|
+
}
|
|
316
|
+
next = next.editScale(scaleEditPatch(scaleId, requestedScale));
|
|
317
|
+
}
|
|
213
318
|
|
|
214
319
|
next = applyMaterializationPlan(
|
|
215
320
|
next,
|
|
@@ -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
|
+
}
|