ggaction 0.0.1
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 +27 -0
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/package.json +84 -0
- package/src/AGENTS.md +119 -0
- package/src/ChartProgram.js +10 -0
- package/src/actions/boxPlots/components.js +88 -0
- package/src/actions/boxPlots/create.js +84 -0
- package/src/actions/boxPlots/index.js +10 -0
- package/src/actions/boxPlots/materialize.js +183 -0
- package/src/actions/boxPlots/options.js +147 -0
- package/src/actions/boxPlots/resolve.js +38 -0
- package/src/actions/canvas/actions.js +144 -0
- package/src/actions/canvas/index.js +6 -0
- package/src/actions/coordinates/actions.js +85 -0
- package/src/actions/coordinates/index.js +5 -0
- package/src/actions/data/box.js +40 -0
- package/src/actions/data/create.js +33 -0
- package/src/actions/data/density.js +62 -0
- package/src/actions/data/derived.js +52 -0
- package/src/actions/data/filter.js +168 -0
- package/src/actions/data/index.js +32 -0
- package/src/actions/data/interval.js +69 -0
- package/src/actions/data/regression.js +71 -0
- package/src/actions/data/shared.js +25 -0
- package/src/actions/encodings/appearance.js +243 -0
- package/src/actions/encodings/barWidth.js +60 -0
- package/src/actions/encodings/color.js +329 -0
- package/src/actions/encodings/density.js +225 -0
- package/src/actions/encodings/histogram.js +50 -0
- package/src/actions/encodings/index.js +23 -0
- package/src/actions/encodings/offset.js +104 -0
- package/src/actions/encodings/position/policies/area.js +24 -0
- package/src/actions/encodings/position/policies/bar.js +110 -0
- package/src/actions/encodings/position/policies/common.js +17 -0
- package/src/actions/encodings/position/policies/index.js +25 -0
- package/src/actions/encodings/position/policies/line.js +74 -0
- package/src/actions/encodings/position/policies/point.js +19 -0
- package/src/actions/encodings/position/policies/rule.js +17 -0
- package/src/actions/encodings/position/resolve.js +170 -0
- package/src/actions/encodings/position.js +176 -0
- package/src/actions/encodings/ranged.js +276 -0
- package/src/actions/encodings/ruleAppearance.js +47 -0
- package/src/actions/encodings/shared.js +164 -0
- package/src/actions/encodings/strokeDash.js +145 -0
- package/src/actions/errorBands/components.js +81 -0
- package/src/actions/errorBands/create.js +159 -0
- package/src/actions/errorBands/index.js +7 -0
- package/src/actions/errorBands/resolve.js +28 -0
- package/src/actions/errorBars/components.js +62 -0
- package/src/actions/errorBars/create.js +144 -0
- package/src/actions/errorBars/index.js +7 -0
- package/src/actions/errorBars/resolve.js +25 -0
- package/src/actions/guides/axes/axes.js +204 -0
- package/src/actions/guides/axes/axis.js +131 -0
- package/src/actions/guides/axes/index.js +17 -0
- package/src/actions/guides/axes/labels.js +231 -0
- package/src/actions/guides/axes/lines.js +149 -0
- package/src/actions/guides/axes/policy.js +155 -0
- package/src/actions/guides/axes/tickGroups.js +149 -0
- package/src/actions/guides/axes/ticks.js +179 -0
- package/src/actions/guides/axes/titles.js +264 -0
- package/src/actions/guides/grids/grid.js +228 -0
- package/src/actions/guides/grids/index.js +1 -0
- package/src/actions/guides/grids/resolve.js +258 -0
- package/src/actions/guides/guides.js +138 -0
- package/src/actions/guides/index.js +11 -0
- package/src/actions/guides/legends/categorical/actions.js +329 -0
- package/src/actions/guides/legends/categorical/components.js +140 -0
- package/src/actions/guides/legends/categorical/index.js +49 -0
- package/src/actions/guides/legends/categorical/layout.js +445 -0
- package/src/actions/guides/legends/categorical/options.js +188 -0
- package/src/actions/guides/legends/categorical/recipes.js +120 -0
- package/src/actions/guides/legends/categorical/resolve.js +131 -0
- package/src/actions/guides/legends/categorical/symbols.js +280 -0
- package/src/actions/guides/legends/categorical/validation.js +27 -0
- package/src/actions/guides/legends/continuous/common.js +273 -0
- package/src/actions/guides/legends/continuous/gradient.js +363 -0
- package/src/actions/guides/legends/continuous/index.js +25 -0
- package/src/actions/guides/legends/continuous/interval.js +234 -0
- package/src/actions/guides/legends/continuous/opacity.js +382 -0
- package/src/actions/guides/legends/edit.js +314 -0
- package/src/actions/guides/legends/index.js +9 -0
- package/src/actions/guides/legends/size.js +220 -0
- package/src/actions/guides/tickValues.js +84 -0
- package/src/actions/index.js +31 -0
- package/src/actions/intervals/resolve.js +352 -0
- package/src/actions/marks/area.js +369 -0
- package/src/actions/marks/bar/create.js +46 -0
- package/src/actions/marks/bar/edit.js +90 -0
- package/src/actions/marks/bar/index.js +9 -0
- package/src/actions/marks/bar/materialize.js +130 -0
- package/src/actions/marks/index.js +13 -0
- package/src/actions/marks/line.js +248 -0
- package/src/actions/marks/point.js +420 -0
- package/src/actions/marks/rule.js +287 -0
- package/src/actions/marks/shared.js +112 -0
- package/src/actions/primitives/createGraphics.js +163 -0
- package/src/actions/primitives/editGraphics.js +208 -0
- package/src/actions/primitives/index.js +9 -0
- package/src/actions/primitives/semantic.js +168 -0
- package/src/actions/primitives/semanticValue.js +182 -0
- package/src/actions/regression/components.js +216 -0
- package/src/actions/regression/create.js +102 -0
- package/src/actions/regression/index.js +23 -0
- package/src/actions/regression/resolve.js +57 -0
- package/src/actions/scales/consumers.js +283 -0
- package/src/actions/scales/create.js +148 -0
- package/src/actions/scales/definitions.js +319 -0
- package/src/actions/scales/edit.js +290 -0
- package/src/actions/scales/index.js +11 -0
- package/src/actions/scales/materialize.js +466 -0
- package/src/actions/scales/quantitativeColor.js +45 -0
- package/src/actions/selection/actions.js +384 -0
- package/src/actions/selection/index.js +23 -0
- package/src/actions/titles/actions.js +235 -0
- package/src/actions/titles/index.js +19 -0
- package/src/actions/titles/resolve.js +321 -0
- package/src/core/ChartProgram.js +316 -0
- package/src/core/action.js +120 -0
- package/src/core/identifiers.js +28 -0
- package/src/core/immutable.js +94 -0
- package/src/core/specs.js +29 -0
- package/src/core/textMetrics.js +23 -0
- package/src/core/validation.js +54 -0
- package/src/core/vocabulary.js +58 -0
- package/src/extension.js +2 -0
- package/src/grammar/aggregate.js +259 -0
- package/src/grammar/areaSeries.js +217 -0
- package/src/grammar/bars/aggregate.js +98 -0
- package/src/grammar/bars/geometry.js +54 -0
- package/src/grammar/bars/policy.js +90 -0
- package/src/grammar/boxPlot.js +140 -0
- package/src/grammar/coordinates.js +25 -0
- package/src/grammar/curveCommands.js +356 -0
- package/src/grammar/density.js +296 -0
- package/src/grammar/filter.js +169 -0
- package/src/grammar/histogram.js +332 -0
- package/src/grammar/interval.js +343 -0
- package/src/grammar/lineSeries.js +197 -0
- package/src/grammar/markFilter.js +46 -0
- package/src/grammar/markSelection.js +326 -0
- package/src/grammar/palettes.js +208 -0
- package/src/grammar/pathCommands.js +86 -0
- package/src/grammar/pointShapes.js +164 -0
- package/src/grammar/positionCompatibility.js +32 -0
- package/src/grammar/regression.js +578 -0
- package/src/grammar/ruleAppearance.js +12 -0
- package/src/grammar/rules.js +90 -0
- package/src/grammar/scales/appearance.js +185 -0
- package/src/grammar/scales/color.js +349 -0
- package/src/grammar/scales/continuous.js +111 -0
- package/src/grammar/scales/definition.js +153 -0
- package/src/grammar/scales/discretized.js +159 -0
- package/src/grammar/scales/fields.js +133 -0
- package/src/grammar/scales/index.js +12 -0
- package/src/grammar/scales/mapping.js +27 -0
- package/src/grammar/scales/ordinal.js +221 -0
- package/src/grammar/scales/policies.js +50 -0
- package/src/grammar/scales/temporal.js +53 -0
- package/src/grammar/scales/transformed.js +236 -0
- package/src/grammar/scales/types.js +102 -0
- package/src/grammar/scales/validation.js +66 -0
- package/src/grammar/scales.js +1 -0
- package/src/grammar/schemas/concreteGraphic.js +73 -0
- package/src/grammar/schemas/graphic.js +131 -0
- package/src/grammar/schemas/graphicBounds.js +146 -0
- package/src/grammar/schemas/graphicTree.js +133 -0
- package/src/grammar/schemas/mark.js +4 -0
- package/src/grammar/schemas/semanticPath.js +174 -0
- package/src/grammar/seriesLayout.js +76 -0
- package/src/grammar/ticks.js +149 -0
- package/src/grammar/transforms.js +37 -0
- package/src/index.js +2 -0
- package/src/layout/canvas.js +114 -0
- package/src/layout/grid.js +17 -0
- package/src/layout/legend.js +51 -0
- package/src/layout/text.js +62 -0
- package/src/layout/title.js +107 -0
- package/src/materialization/bars/aggregate.js +184 -0
- package/src/materialization/bars/grouped.js +117 -0
- package/src/materialization/bars/histogram.js +163 -0
- package/src/materialization/bars/ranged.js +53 -0
- package/src/materialization/bars/resolve.js +56 -0
- package/src/materialization/dataProvenance.js +15 -0
- package/src/materialization/density.js +29 -0
- package/src/materialization/dependencies.js +134 -0
- package/src/materialization/encodings.js +50 -0
- package/src/materialization/graphicHierarchy.js +104 -0
- package/src/materialization/legends.js +20 -0
- package/src/materialization/marks.js +110 -0
- package/src/materialization/planner.js +38 -0
- package/src/materialization/selection/filter.js +40 -0
- package/src/materialization/selection/items.js +503 -0
- package/src/materialization/selection/path.js +37 -0
- package/src/materialization/selection/point.js +118 -0
- package/src/materialization/selection/policies/area.js +10 -0
- package/src/materialization/selection/policies/bar.js +10 -0
- package/src/materialization/selection/policies/index.js +40 -0
- package/src/materialization/selection/policies/line.js +10 -0
- package/src/materialization/selection/policies/point.js +10 -0
- package/src/materialization/selection/policies/rule.js +10 -0
- package/src/materialization/selection/state.js +67 -0
- package/src/materialization/selection/styles.js +183 -0
- package/src/renderers/canvas/circle.js +62 -0
- package/src/renderers/canvas/index.js +132 -0
- package/src/renderers/canvas/line.js +54 -0
- package/src/renderers/canvas/path.js +98 -0
- package/src/renderers/canvas/rect.js +57 -0
- package/src/renderers/canvas/text.js +96 -0
- package/src/renderers/canvas/validation.js +23 -0
- package/src/renderers/png.js +29 -0
- package/src/selectors/coordinates.js +13 -0
- package/src/selectors/datasets.js +13 -0
- package/src/selectors/index.js +4 -0
- package/src/selectors/layers.js +32 -0
- package/src/selectors/scales.js +21 -0
- package/src/theme/defaults.js +13 -0
- package/types/extension.d.ts +14 -0
- package/types/index.d.ts +81 -0
- package/types/png.d.ts +14 -0
- package/types/program.d.ts +1096 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { emptyPositionPolicy } from "./common.js";
|
|
2
|
+
|
|
3
|
+
export function resolveRulePositionPolicy({ args, fieldType }) {
|
|
4
|
+
if (!["quantitative", "temporal", "ordinal", "nominal"].includes(fieldType)) {
|
|
5
|
+
throw new Error("Rule position encoding requires a supported field type.");
|
|
6
|
+
}
|
|
7
|
+
if (args.aggregate !== undefined) {
|
|
8
|
+
throw new Error("Rule position encoding does not support aggregate.");
|
|
9
|
+
}
|
|
10
|
+
if (args.bin !== undefined) {
|
|
11
|
+
throw new Error("Rule position encoding does not support bin.");
|
|
12
|
+
}
|
|
13
|
+
if (args.stack !== undefined) {
|
|
14
|
+
throw new Error("Rule position encoding does not support stack.");
|
|
15
|
+
}
|
|
16
|
+
return emptyPositionPolicy();
|
|
17
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { validateUserId } from "../../../core/identifiers.js";
|
|
2
|
+
import { getPositionCoordinateDefaults } from "../../../grammar/coordinates.js";
|
|
3
|
+
import {
|
|
4
|
+
readNominalField,
|
|
5
|
+
readQuantitativeField,
|
|
6
|
+
readScaleField,
|
|
7
|
+
readTemporalField,
|
|
8
|
+
validateFieldType,
|
|
9
|
+
validatePositionChannel
|
|
10
|
+
} from "../../../grammar/scales.js";
|
|
11
|
+
import { resolvePositionScaleDefinition } from "../../scales/definitions.js";
|
|
12
|
+
import { findCoordinate } from "../../../selectors/coordinates.js";
|
|
13
|
+
import {
|
|
14
|
+
resolveReassignmentScaleOptions,
|
|
15
|
+
resolveTarget,
|
|
16
|
+
validateOptions
|
|
17
|
+
} from "../shared.js";
|
|
18
|
+
import {
|
|
19
|
+
isAggregate,
|
|
20
|
+
validateAggregateFieldType,
|
|
21
|
+
validateAggregateFieldValues,
|
|
22
|
+
} from "../../../grammar/aggregate.js";
|
|
23
|
+
import { normalizeRuleDatum } from "../../../grammar/rules.js";
|
|
24
|
+
import { resolveMarkPositionPolicy } from "./policies/index.js";
|
|
25
|
+
|
|
26
|
+
const POSITION_ENCODING_OPTIONS = Object.freeze([
|
|
27
|
+
"field", "datum", "target", "fieldType", "scale", "coordinate",
|
|
28
|
+
"aggregate", "bin", "stack"
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
function resolveCoordinate(program, channel, layer, requestedId) {
|
|
32
|
+
const defaults = getPositionCoordinateDefaults(channel);
|
|
33
|
+
const existingId = layer.coordinate;
|
|
34
|
+
if (requestedId !== undefined) validateUserId(requestedId, "Coordinate id");
|
|
35
|
+
if (existingId !== undefined && requestedId !== undefined && existingId !== requestedId) {
|
|
36
|
+
throw new Error(`Layer "${layer.id}" already uses coordinate "${existingId}".`);
|
|
37
|
+
}
|
|
38
|
+
const id = existingId ?? requestedId ?? defaults.id;
|
|
39
|
+
const coordinate = findCoordinate(program, id);
|
|
40
|
+
if (coordinate !== undefined && coordinate.type !== defaults.type) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`${channel} encoding requires a ${defaults.type} coordinate, but "${id}" is ${coordinate.type}.`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return { id, type: defaults.type };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function resolvePositionEncoding(program, channel, args, operation) {
|
|
49
|
+
validateOptions(args, POSITION_ENCODING_OPTIONS, operation);
|
|
50
|
+
validatePositionChannel(channel);
|
|
51
|
+
const { id: target, dataset, layer } = resolveTarget(
|
|
52
|
+
program,
|
|
53
|
+
args.target,
|
|
54
|
+
["point", "line", "bar", "area", "rule"]
|
|
55
|
+
);
|
|
56
|
+
const hasField = Object.hasOwn(args, "field");
|
|
57
|
+
const hasDatum = Object.hasOwn(args, "datum");
|
|
58
|
+
if (layer.mark.type === "rule") {
|
|
59
|
+
if (hasField === hasDatum) {
|
|
60
|
+
throw new Error(`${operation} requires exactly one of field or datum for a rule mark.`);
|
|
61
|
+
}
|
|
62
|
+
if (args.fieldType === undefined) {
|
|
63
|
+
throw new Error(`${operation} requires fieldType for a rule mark.`);
|
|
64
|
+
}
|
|
65
|
+
} else if (hasDatum) {
|
|
66
|
+
throw new Error(`${operation} does not support datum for a ${layer.mark.type} mark.`);
|
|
67
|
+
}
|
|
68
|
+
const previous = layer.encoding?.[channel];
|
|
69
|
+
const requestedFieldType =
|
|
70
|
+
args.fieldType ?? previous?.fieldType ?? "quantitative";
|
|
71
|
+
const fieldType = requestedFieldType === "nominal"
|
|
72
|
+
? "nominal"
|
|
73
|
+
: validateFieldType(requestedFieldType);
|
|
74
|
+
const xEncoding = layer.encoding?.x;
|
|
75
|
+
const field = layer.mark.type === "bar" && channel === "y" &&
|
|
76
|
+
xEncoding?.bin !== undefined && args.field === undefined
|
|
77
|
+
? xEncoding.field
|
|
78
|
+
: args.field;
|
|
79
|
+
const datum = args.datum;
|
|
80
|
+
const effectiveArgs = { ...args };
|
|
81
|
+
for (const property of ["aggregate", "bin", "stack"]) {
|
|
82
|
+
if (!Object.hasOwn(effectiveArgs, property) && previous !== undefined &&
|
|
83
|
+
Object.hasOwn(previous, property)) {
|
|
84
|
+
effectiveArgs[property] = previous[property];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const policy = resolveMarkPositionPolicy({
|
|
88
|
+
program,
|
|
89
|
+
layer,
|
|
90
|
+
dataset,
|
|
91
|
+
channel,
|
|
92
|
+
args: effectiveArgs,
|
|
93
|
+
fieldType,
|
|
94
|
+
field
|
|
95
|
+
});
|
|
96
|
+
const usesField = layer.mark.type !== "rule" || hasField;
|
|
97
|
+
if (usesField && (typeof field !== "string" || field.length === 0)) {
|
|
98
|
+
throw new TypeError(`${operation} field must be a non-empty string.`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const aggregateOutput = isAggregate(policy.aggregate);
|
|
102
|
+
const requestedScale = resolveReassignmentScaleOptions(
|
|
103
|
+
previous,
|
|
104
|
+
Object.hasOwn(args, "scale") ? args.scale : {}
|
|
105
|
+
);
|
|
106
|
+
const scale = resolvePositionScaleDefinition(
|
|
107
|
+
program,
|
|
108
|
+
channel,
|
|
109
|
+
aggregateOutput ? "quantitative" : fieldType,
|
|
110
|
+
requestedScale,
|
|
111
|
+
layer.mark.type === "bar"
|
|
112
|
+
? program.markConfigs[target]?.boxPlot !== undefined && fieldType === "quantitative"
|
|
113
|
+
? { nice: true, zero: false }
|
|
114
|
+
: fieldType === "quantitative"
|
|
115
|
+
? policy.bin !== undefined || policy.stack === null
|
|
116
|
+
? { nice: true, zero: false }
|
|
117
|
+
: { nice: true, zero: true }
|
|
118
|
+
: fieldType === "temporal"
|
|
119
|
+
? { nice: true }
|
|
120
|
+
: { discreteType: "band" }
|
|
121
|
+
: ["ordinal", "nominal"].includes(fieldType)
|
|
122
|
+
? { discreteType: "point" }
|
|
123
|
+
: {}
|
|
124
|
+
);
|
|
125
|
+
if (Object.hasOwn(scale, "unknown") && layer.mark.type !== "point") {
|
|
126
|
+
throw new Error(
|
|
127
|
+
"Position scale unknown currently requires a row-owned point mark."
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
if (layer.mark.type === "rule" && hasDatum) {
|
|
131
|
+
normalizeRuleDatum(datum, fieldType, channel);
|
|
132
|
+
} else if (aggregateOutput) {
|
|
133
|
+
validateAggregateFieldType(policy.aggregate, fieldType);
|
|
134
|
+
validateAggregateFieldValues(dataset.values, field, fieldType);
|
|
135
|
+
} else if (program.markConfigs[target]?.boxPlot !== undefined) {
|
|
136
|
+
for (const [index, row] of dataset.values.entries()) {
|
|
137
|
+
const value = row[field];
|
|
138
|
+
if (value === undefined || value === null || value === "") continue;
|
|
139
|
+
if (fieldType === "quantitative" && !Number.isFinite(value)) {
|
|
140
|
+
throw new TypeError(`Field "${field}" must contain a finite number at row ${index}.`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} else if (Object.hasOwn(scale, "unknown")) {
|
|
144
|
+
readScaleField(dataset.values, field, fieldType, { allowUnknown: true });
|
|
145
|
+
} else if (fieldType === "temporal") readTemporalField(dataset.values, field);
|
|
146
|
+
else if (["ordinal", "nominal"].includes(fieldType)) {
|
|
147
|
+
readNominalField(dataset.values, field);
|
|
148
|
+
} else readQuantitativeField(dataset.values, field);
|
|
149
|
+
|
|
150
|
+
if (
|
|
151
|
+
layer.mark.type === "bar" &&
|
|
152
|
+
["ordinal", "nominal"].includes(fieldType) &&
|
|
153
|
+
scale.type === "point"
|
|
154
|
+
) {
|
|
155
|
+
throw new Error("Categorical bar positions require a band scale.");
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
target,
|
|
159
|
+
layer,
|
|
160
|
+
previous,
|
|
161
|
+
requestedScale,
|
|
162
|
+
field,
|
|
163
|
+
datum,
|
|
164
|
+
hasField: usesField,
|
|
165
|
+
fieldType,
|
|
166
|
+
scale,
|
|
167
|
+
coordinate: resolveCoordinate(program, channel, layer, args.coordinate),
|
|
168
|
+
...policy
|
|
169
|
+
};
|
|
170
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import {
|
|
3
|
+
canMaterializeArea,
|
|
4
|
+
canMaterializeLine,
|
|
5
|
+
} from "../../materialization/marks.js";
|
|
6
|
+
import { resolveBarGrain } from "../../grammar/bars/policy.js";
|
|
7
|
+
import { resolvePositionEncoding } from "./position/resolve.js";
|
|
8
|
+
import { findLayer } from "../../selectors/layers.js";
|
|
9
|
+
import {
|
|
10
|
+
applyEncodingScale,
|
|
11
|
+
rebindPositionGuides
|
|
12
|
+
} from "./shared.js";
|
|
13
|
+
|
|
14
|
+
function encodePosition(program, channel, args, operation) {
|
|
15
|
+
const {
|
|
16
|
+
target,
|
|
17
|
+
layer,
|
|
18
|
+
previous,
|
|
19
|
+
requestedScale,
|
|
20
|
+
field,
|
|
21
|
+
datum,
|
|
22
|
+
hasField,
|
|
23
|
+
fieldType,
|
|
24
|
+
scale,
|
|
25
|
+
coordinate,
|
|
26
|
+
bin,
|
|
27
|
+
aggregate,
|
|
28
|
+
stack
|
|
29
|
+
} = resolvePositionEncoding(program, channel, args, operation);
|
|
30
|
+
|
|
31
|
+
let next = program
|
|
32
|
+
.createCoordinate({
|
|
33
|
+
id: coordinate.id,
|
|
34
|
+
type: coordinate.type,
|
|
35
|
+
layers: [target]
|
|
36
|
+
});
|
|
37
|
+
if (previous !== undefined) {
|
|
38
|
+
const alternate = hasField ? "datum" : "field";
|
|
39
|
+
if (Object.hasOwn(previous, alternate)) {
|
|
40
|
+
next = next.editSemantic({
|
|
41
|
+
property: `layer[${target}].encoding.${channel}.${alternate}`,
|
|
42
|
+
remove: true
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
next = next
|
|
47
|
+
.editSemantic({
|
|
48
|
+
property: `layer[${target}].encoding.${channel}.${hasField ? "field" : "datum"}`,
|
|
49
|
+
value: hasField ? field : datum
|
|
50
|
+
})
|
|
51
|
+
.editSemantic({
|
|
52
|
+
property: `layer[${target}].encoding.${channel}.fieldType`,
|
|
53
|
+
value: fieldType
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (layer.mark.type === "line" && channel === "y" && args.aggregate !== undefined) {
|
|
57
|
+
next = next.editSemantic({
|
|
58
|
+
property: `layer[${target}].encoding.y.aggregate`,
|
|
59
|
+
value: aggregate
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (layer.mark.type === "bar" && channel === "x" && bin !== undefined) {
|
|
64
|
+
const [mode] = Object.keys(bin);
|
|
65
|
+
const previousModes = Object.keys(layer.encoding?.x?.bin ?? {});
|
|
66
|
+
for (const previousMode of previousModes) {
|
|
67
|
+
if (previousMode === mode) continue;
|
|
68
|
+
next = next.editSemantic({
|
|
69
|
+
property: `layer[${target}].encoding.x.bin.${previousMode}`,
|
|
70
|
+
remove: true
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
next = next.editSemantic({
|
|
74
|
+
property: `layer[${target}].encoding.x.bin.${mode}`,
|
|
75
|
+
value: bin[mode]
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (layer.mark.type === "bar") {
|
|
80
|
+
if (aggregate !== undefined) {
|
|
81
|
+
next = next.editSemantic({
|
|
82
|
+
property: `layer[${target}].encoding.${channel}.aggregate`,
|
|
83
|
+
value: aggregate
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
if (stack !== undefined) {
|
|
87
|
+
next = next.editSemantic({
|
|
88
|
+
property: `layer[${target}].encoding.${channel}.stack`,
|
|
89
|
+
value: stack
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (layer.mark.type === "area" && channel === "y" && stack !== undefined) {
|
|
95
|
+
next = next.editSemantic({
|
|
96
|
+
property: `layer[${target}].encoding.y.stack`,
|
|
97
|
+
value: stack
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
next = next.editSemantic({
|
|
102
|
+
property: `layer[${target}].encoding.${channel}.scale`,
|
|
103
|
+
value: scale.id
|
|
104
|
+
});
|
|
105
|
+
next = applyEncodingScale(next, scale, requestedScale, {
|
|
106
|
+
reassignment: previous?.scale === scale.id
|
|
107
|
+
});
|
|
108
|
+
next = rebindPositionGuides(
|
|
109
|
+
next,
|
|
110
|
+
channel,
|
|
111
|
+
previous?.scale,
|
|
112
|
+
scale.id,
|
|
113
|
+
target
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
if (layer.mark.type === "line") {
|
|
117
|
+
const updated = findLayer(next, target);
|
|
118
|
+
return canMaterializeLine(next, updated)
|
|
119
|
+
? next.rematerializeLineMark({ id: target })
|
|
120
|
+
: next.rematerializeScale({ id: scale.id });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (layer.mark.type === "rule") {
|
|
124
|
+
return next.rematerializeRuleMark({ id: target });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (layer.mark.type === "bar") {
|
|
128
|
+
const updated = findLayer(next, target);
|
|
129
|
+
const pendingBox = next.markConfigs[target]?.boxPlot;
|
|
130
|
+
if (pendingBox !== undefined && !pendingBox.materialized) {
|
|
131
|
+
return updated.encoding?.x !== undefined && updated.encoding?.y !== undefined
|
|
132
|
+
? next.materializeBoxPlot({ id: target })
|
|
133
|
+
: next;
|
|
134
|
+
}
|
|
135
|
+
const materialized = resolveBarGrain(updated) !== undefined
|
|
136
|
+
? next.rematerializeBarMark({ id: target })
|
|
137
|
+
: next.rematerializeScale({ id: scale.id });
|
|
138
|
+
return materialized;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
next = next.rematerializeScale({ id: scale.id });
|
|
142
|
+
if (layer.mark.type === "area") {
|
|
143
|
+
const updated = findLayer(next, target);
|
|
144
|
+
return canMaterializeArea(next, updated)
|
|
145
|
+
? next.rematerializeAreaMark({ id: target })
|
|
146
|
+
: next;
|
|
147
|
+
}
|
|
148
|
+
return layer.mark.type === "point"
|
|
149
|
+
? next.rematerializePointMark({ id: target })
|
|
150
|
+
: next;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const encodeX = action(
|
|
154
|
+
{
|
|
155
|
+
op: "encodeX",
|
|
156
|
+
description: "Encode a field as horizontal position."
|
|
157
|
+
},
|
|
158
|
+
function (args = {}) {
|
|
159
|
+
return encodePosition(this, "x", args, "encodeX");
|
|
160
|
+
}
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const encodeY = action(
|
|
164
|
+
{
|
|
165
|
+
op: "encodeY",
|
|
166
|
+
description: "Encode a field as vertical position."
|
|
167
|
+
},
|
|
168
|
+
function (args = {}) {
|
|
169
|
+
return encodePosition(this, "y", args, "encodeY");
|
|
170
|
+
}
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
export function registerPositionEncodingActions(ProgramClass) {
|
|
174
|
+
ProgramClass.prototype.encodeX = encodeX;
|
|
175
|
+
ProgramClass.prototype.encodeY = encodeY;
|
|
176
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import { validateUserId } from "../../core/identifiers.js";
|
|
3
|
+
import { isPlainObject } from "../../core/immutable.js";
|
|
4
|
+
import {
|
|
5
|
+
readNominalField,
|
|
6
|
+
readQuantitativeField,
|
|
7
|
+
readTemporalField,
|
|
8
|
+
validateSemanticFieldType
|
|
9
|
+
} from "../../grammar/scales.js";
|
|
10
|
+
import { normalizeRuleDatum } from "../../grammar/rules.js";
|
|
11
|
+
import {
|
|
12
|
+
canMaterializeArea,
|
|
13
|
+
canMaterializeLine
|
|
14
|
+
} from "../../materialization/marks.js";
|
|
15
|
+
import { findLayer } from "../../selectors/layers.js";
|
|
16
|
+
import {
|
|
17
|
+
resolveTarget,
|
|
18
|
+
validateLineSeriesCompatibility,
|
|
19
|
+
validateOptions
|
|
20
|
+
} from "./shared.js";
|
|
21
|
+
|
|
22
|
+
const SECONDARY_OPTIONS = Object.freeze([
|
|
23
|
+
"field", "datum", "target", "fieldType", "scale", "coordinate"
|
|
24
|
+
]);
|
|
25
|
+
const RANGE_OPTIONS = Object.freeze([
|
|
26
|
+
"lower", "upper", "target", "fieldType", "coordinate", "scale"
|
|
27
|
+
]);
|
|
28
|
+
const GROUP_OPTIONS = Object.freeze(["field", "target", "fieldType"]);
|
|
29
|
+
|
|
30
|
+
function validateSecondaryScale(args, primaryScale, axisLabel) {
|
|
31
|
+
if (args.scale === undefined) return;
|
|
32
|
+
if (!isPlainObject(args.scale)) {
|
|
33
|
+
throw new TypeError("Encoding scale must be a plain object.");
|
|
34
|
+
}
|
|
35
|
+
validateOptions(args.scale, ["id"], "scale");
|
|
36
|
+
if (args.scale.id !== undefined && args.scale.id !== primaryScale) {
|
|
37
|
+
throw new Error(`${axisLabel} primary and secondary endpoints must share one scale.`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function validateSecondaryField(dataset, field, fieldType) {
|
|
42
|
+
if (["nominal", "ordinal"].includes(fieldType)) {
|
|
43
|
+
readNominalField(dataset.values, field);
|
|
44
|
+
} else if (fieldType === "temporal") {
|
|
45
|
+
readTemporalField(dataset.values, field);
|
|
46
|
+
} else {
|
|
47
|
+
readQuantitativeField(dataset.values, field);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function encodeSecondaryPosition(program, channel, args, operation, types) {
|
|
52
|
+
validateOptions(args, SECONDARY_OPTIONS, operation);
|
|
53
|
+
const { id: target, dataset, layer } = resolveTarget(
|
|
54
|
+
program,
|
|
55
|
+
args.target,
|
|
56
|
+
types,
|
|
57
|
+
types.length === 1 ? `${types[0]} mark` : "ranged mark"
|
|
58
|
+
);
|
|
59
|
+
const primaryChannel = channel === "x2" ? "x" : "y";
|
|
60
|
+
const primary = layer.encoding?.[primaryChannel];
|
|
61
|
+
if (primary?.scale === undefined) {
|
|
62
|
+
throw new Error(`${operation} requires an existing ${primaryChannel} encoding.`);
|
|
63
|
+
}
|
|
64
|
+
if (args.coordinate !== undefined) {
|
|
65
|
+
const coordinate = validateUserId(args.coordinate, "Coordinate id");
|
|
66
|
+
if (coordinate !== layer.coordinate) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`${operation} must use the primary ${primaryChannel} coordinate "${layer.coordinate}".`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
validateSecondaryScale(args, primary.scale, primaryChannel);
|
|
73
|
+
|
|
74
|
+
const rule = layer.mark.type === "rule";
|
|
75
|
+
const hasField = Object.hasOwn(args, "field");
|
|
76
|
+
const hasDatum = Object.hasOwn(args, "datum");
|
|
77
|
+
if (rule && hasField === hasDatum) {
|
|
78
|
+
throw new Error(`${operation} requires exactly one of field or datum for a rule mark.`);
|
|
79
|
+
}
|
|
80
|
+
if (!rule && (!hasField || hasDatum)) {
|
|
81
|
+
throw new Error(`${operation} requires a field for a ranged mark.`);
|
|
82
|
+
}
|
|
83
|
+
if (rule && args.fieldType === undefined) {
|
|
84
|
+
throw new Error(`${operation} requires fieldType for a rule mark.`);
|
|
85
|
+
}
|
|
86
|
+
const fieldType = rule
|
|
87
|
+
? validateSemanticFieldType(args.fieldType)
|
|
88
|
+
: args.fieldType ?? "quantitative";
|
|
89
|
+
if (!rule && fieldType !== "quantitative") {
|
|
90
|
+
throw new Error(`${operation} requires a quantitative field.`);
|
|
91
|
+
}
|
|
92
|
+
if (rule && fieldType !== primary.fieldType) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`${operation} fieldType must match the primary ${primaryChannel} fieldType.`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
if (hasField) validateSecondaryField(dataset, args.field, fieldType);
|
|
98
|
+
else normalizeRuleDatum(args.datum, fieldType, channel);
|
|
99
|
+
|
|
100
|
+
const previous = layer.encoding?.[channel];
|
|
101
|
+
let next = program;
|
|
102
|
+
if (layer.mark.type === "bar") {
|
|
103
|
+
for (const property of ["aggregate", "stack", "bin"]) {
|
|
104
|
+
if (layer.encoding?.[primaryChannel]?.[property] !== undefined) {
|
|
105
|
+
next = next.editSemantic({
|
|
106
|
+
property: `layer[${target}].encoding.${primaryChannel}.${property}`,
|
|
107
|
+
remove: true
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (previous !== undefined) {
|
|
113
|
+
const alternate = hasField ? "datum" : "field";
|
|
114
|
+
if (Object.hasOwn(previous, alternate)) {
|
|
115
|
+
next = next.editSemantic({
|
|
116
|
+
property: `layer[${target}].encoding.${channel}.${alternate}`,
|
|
117
|
+
remove: true
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
next = next
|
|
122
|
+
.editSemantic({
|
|
123
|
+
property: `layer[${target}].encoding.${channel}.${hasField ? "field" : "datum"}`,
|
|
124
|
+
value: hasField ? args.field : args.datum
|
|
125
|
+
})
|
|
126
|
+
.editSemantic({
|
|
127
|
+
property: `layer[${target}].encoding.${channel}.fieldType`,
|
|
128
|
+
value: fieldType
|
|
129
|
+
})
|
|
130
|
+
.editSemantic({
|
|
131
|
+
property: `layer[${target}].encoding.${channel}.scale`,
|
|
132
|
+
value: primary.scale
|
|
133
|
+
})
|
|
134
|
+
.rematerializeScale({ id: primary.scale });
|
|
135
|
+
if (rule) return next.rematerializeRuleMark({ id: target });
|
|
136
|
+
return layer.mark.type === "bar"
|
|
137
|
+
? next.rematerializeBarMark({ id: target })
|
|
138
|
+
: next.rematerializeAreaMark({ id: target });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const encodeX2 = action(
|
|
142
|
+
{
|
|
143
|
+
op: "encodeX2",
|
|
144
|
+
description: "Encode a secondary horizontal endpoint."
|
|
145
|
+
},
|
|
146
|
+
function (args = {}) {
|
|
147
|
+
return encodeSecondaryPosition(
|
|
148
|
+
this,
|
|
149
|
+
"x2",
|
|
150
|
+
args,
|
|
151
|
+
"encodeX2",
|
|
152
|
+
["area", "rule", "bar"]
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const encodeY2 = action(
|
|
158
|
+
{
|
|
159
|
+
op: "encodeY2",
|
|
160
|
+
description: "Encode a secondary vertical endpoint."
|
|
161
|
+
},
|
|
162
|
+
function (args = {}) {
|
|
163
|
+
return encodeSecondaryPosition(
|
|
164
|
+
this,
|
|
165
|
+
"y2",
|
|
166
|
+
args,
|
|
167
|
+
"encodeY2",
|
|
168
|
+
["area", "rule", "bar"]
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const encodeYRange = action(
|
|
174
|
+
{
|
|
175
|
+
op: "encodeYRange",
|
|
176
|
+
description: "Atomically encode lower and upper area bounds."
|
|
177
|
+
},
|
|
178
|
+
function (args = {}) {
|
|
179
|
+
validateOptions(args, RANGE_OPTIONS, "encodeYRange");
|
|
180
|
+
const target = args.target;
|
|
181
|
+
const lower = this.encodeY({
|
|
182
|
+
field: args.lower,
|
|
183
|
+
...(target === undefined ? {} : { target }),
|
|
184
|
+
fieldType: args.fieldType ?? "quantitative",
|
|
185
|
+
...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }),
|
|
186
|
+
...(args.scale === undefined ? {} : { scale: args.scale })
|
|
187
|
+
});
|
|
188
|
+
return lower.encodeY2({
|
|
189
|
+
field: args.upper,
|
|
190
|
+
...(target === undefined ? {} : { target }),
|
|
191
|
+
fieldType: args.fieldType ?? "quantitative"
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const encodeXRange = action(
|
|
197
|
+
{
|
|
198
|
+
op: "encodeXRange",
|
|
199
|
+
description: "Atomically encode lower and upper horizontal area bounds."
|
|
200
|
+
},
|
|
201
|
+
function (args = {}) {
|
|
202
|
+
validateOptions(args, RANGE_OPTIONS, "encodeXRange");
|
|
203
|
+
const target = args.target;
|
|
204
|
+
const lower = this.encodeX({
|
|
205
|
+
field: args.lower,
|
|
206
|
+
...(target === undefined ? {} : { target }),
|
|
207
|
+
fieldType: args.fieldType ?? "quantitative",
|
|
208
|
+
...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }),
|
|
209
|
+
...(args.scale === undefined ? {} : { scale: args.scale })
|
|
210
|
+
});
|
|
211
|
+
return lower.encodeX2({
|
|
212
|
+
field: args.upper,
|
|
213
|
+
...(target === undefined ? {} : { target }),
|
|
214
|
+
fieldType: args.fieldType ?? "quantitative"
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const encodeGroup = action(
|
|
220
|
+
{
|
|
221
|
+
op: "encodeGroup",
|
|
222
|
+
description: "Split path geometry by a nominal field without a scale."
|
|
223
|
+
},
|
|
224
|
+
function (args = {}) {
|
|
225
|
+
validateOptions(args, GROUP_OPTIONS, "encodeGroup");
|
|
226
|
+
const { id: target, dataset, layer } = resolveTarget(
|
|
227
|
+
this,
|
|
228
|
+
args.target,
|
|
229
|
+
["line", "area"],
|
|
230
|
+
"path mark"
|
|
231
|
+
);
|
|
232
|
+
if ((args.fieldType ?? "nominal") !== "nominal") {
|
|
233
|
+
throw new Error("encodeGroup requires a nominal field.");
|
|
234
|
+
}
|
|
235
|
+
const densityTransform = dataset.transform?.length === 1 &&
|
|
236
|
+
dataset.transform[0].type === "density"
|
|
237
|
+
? dataset.transform[0]
|
|
238
|
+
: undefined;
|
|
239
|
+
if (densityTransform !== undefined && densityTransform.groupBy !== args.field) {
|
|
240
|
+
throw new Error(
|
|
241
|
+
densityTransform.groupBy === undefined
|
|
242
|
+
? `Ungrouped density area mark "${target}" cannot encode group.`
|
|
243
|
+
: `Density area mark "${target}" must group by "${densityTransform.groupBy}".`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
readNominalField(dataset.values, args.field);
|
|
247
|
+
validateLineSeriesCompatibility(layer, "group", args.field);
|
|
248
|
+
const next = this
|
|
249
|
+
.editSemantic({
|
|
250
|
+
property: `layer[${target}].encoding.group.field`,
|
|
251
|
+
value: args.field
|
|
252
|
+
})
|
|
253
|
+
.editSemantic({
|
|
254
|
+
property: `layer[${target}].encoding.group.fieldType`,
|
|
255
|
+
value: "nominal"
|
|
256
|
+
});
|
|
257
|
+
if (layer.mark.type === "area") {
|
|
258
|
+
const updated = findLayer(next, target);
|
|
259
|
+
return canMaterializeArea(next, updated)
|
|
260
|
+
? next.rematerializeAreaMark({ id: target })
|
|
261
|
+
: next;
|
|
262
|
+
}
|
|
263
|
+
const updated = findLayer(next, target);
|
|
264
|
+
return canMaterializeLine(next, updated)
|
|
265
|
+
? next.rematerializeLineMark({ id: target })
|
|
266
|
+
: next;
|
|
267
|
+
}
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
export function registerRangedEncodingActions(ProgramClass) {
|
|
271
|
+
ProgramClass.prototype.encodeX2 = encodeX2;
|
|
272
|
+
ProgramClass.prototype.encodeY2 = encodeY2;
|
|
273
|
+
ProgramClass.prototype.encodeXRange = encodeXRange;
|
|
274
|
+
ProgramClass.prototype.encodeYRange = encodeYRange;
|
|
275
|
+
ProgramClass.prototype.encodeGroup = encodeGroup;
|
|
276
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import {
|
|
3
|
+
validateRuleStroke,
|
|
4
|
+
validateRuleStrokeWidth
|
|
5
|
+
} from "../../grammar/ruleAppearance.js";
|
|
6
|
+
import { resolveTarget, validateOptions } from "./shared.js";
|
|
7
|
+
|
|
8
|
+
const OPTIONS = Object.freeze(["target", "value"]);
|
|
9
|
+
|
|
10
|
+
const encodeStroke = action(
|
|
11
|
+
{
|
|
12
|
+
op: "encodeStroke",
|
|
13
|
+
description: "Set a constant graphical stroke on a rule mark."
|
|
14
|
+
},
|
|
15
|
+
function (args = {}) {
|
|
16
|
+
validateOptions(args, OPTIONS, "encodeStroke");
|
|
17
|
+
const { id } = resolveTarget(this, args.target, ["rule"], "rule mark");
|
|
18
|
+
return this
|
|
19
|
+
._withMarkConfig(id, {
|
|
20
|
+
...this.markConfigs[id],
|
|
21
|
+
stroke: validateRuleStroke(args.value)
|
|
22
|
+
})
|
|
23
|
+
.rematerializeRuleMark({ id });
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const encodeStrokeWidth = action(
|
|
28
|
+
{
|
|
29
|
+
op: "encodeStrokeWidth",
|
|
30
|
+
description: "Set a constant graphical stroke width on a rule mark."
|
|
31
|
+
},
|
|
32
|
+
function (args = {}) {
|
|
33
|
+
validateOptions(args, OPTIONS, "encodeStrokeWidth");
|
|
34
|
+
const { id } = resolveTarget(this, args.target, ["rule"], "rule mark");
|
|
35
|
+
return this
|
|
36
|
+
._withMarkConfig(id, {
|
|
37
|
+
...this.markConfigs[id],
|
|
38
|
+
strokeWidth: validateRuleStrokeWidth(args.value)
|
|
39
|
+
})
|
|
40
|
+
.rematerializeRuleMark({ id });
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
export function registerRuleAppearanceEncodingActions(ProgramClass) {
|
|
45
|
+
ProgramClass.prototype.encodeStroke = encodeStroke;
|
|
46
|
+
ProgramClass.prototype.encodeStrokeWidth = encodeStrokeWidth;
|
|
47
|
+
}
|