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,225 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import { validateUserId } from "../../core/identifiers.js";
|
|
3
|
+
import { isPlainObject } from "../../core/immutable.js";
|
|
4
|
+
import {
|
|
5
|
+
findDataset,
|
|
6
|
+
findLayer,
|
|
7
|
+
hasDataset,
|
|
8
|
+
resolveEligibleLayer
|
|
9
|
+
} from "../../selectors/index.js";
|
|
10
|
+
import { applyMaterializationPlan } from
|
|
11
|
+
"../../materialization/dependencies.js";
|
|
12
|
+
import { planDensityRematerialization } from
|
|
13
|
+
"../../materialization/density.js";
|
|
14
|
+
import { validateOptions } from "./shared.js";
|
|
15
|
+
|
|
16
|
+
const OPTIONS = Object.freeze([
|
|
17
|
+
"field", "target", "source", "groupBy", "bandwidth", "extent", "steps",
|
|
18
|
+
"kernel", "normalization", "as", "densityChannel", "coordinate",
|
|
19
|
+
"valueScale", "densityScale"
|
|
20
|
+
]);
|
|
21
|
+
const EDIT_OPTIONS = Object.freeze([
|
|
22
|
+
"target", "bandwidth", "extent", "steps", "kernel", "normalization"
|
|
23
|
+
]);
|
|
24
|
+
const EDITABLE = Object.freeze(EDIT_OPTIONS.filter(option => option !== "target"));
|
|
25
|
+
|
|
26
|
+
function findArea(program, requested) {
|
|
27
|
+
const areas = program.semanticSpec.layers.filter(layer => layer.mark?.type === "area");
|
|
28
|
+
if (requested !== undefined) {
|
|
29
|
+
const id = validateUserId(requested, "Density target id");
|
|
30
|
+
const selected = findLayer(program, id);
|
|
31
|
+
if (selected === undefined || !areas.includes(selected)) {
|
|
32
|
+
throw new Error(`Unknown density area target "${id}".`);
|
|
33
|
+
}
|
|
34
|
+
return selected;
|
|
35
|
+
}
|
|
36
|
+
const current = findLayer(program, program.context.currentMark);
|
|
37
|
+
if (current !== undefined && areas.includes(current)) return current;
|
|
38
|
+
if (areas.length === 1) return areas[0];
|
|
39
|
+
if (areas.length === 0) {
|
|
40
|
+
throw new Error("encodeDensity requires an eligible area mark.");
|
|
41
|
+
}
|
|
42
|
+
throw new Error("encodeDensity target is ambiguous; provide target.");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function requireField(value, label) {
|
|
46
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
47
|
+
throw new TypeError(`${label} must be a non-empty string.`);
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function scaleOptions(value, defaults, label) {
|
|
53
|
+
if (value !== undefined && !isPlainObject(value)) {
|
|
54
|
+
throw new TypeError(`${label} must be a plain object.`);
|
|
55
|
+
}
|
|
56
|
+
return { ...defaults, ...(value ?? {}) };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const encodeDensity = action(
|
|
60
|
+
{
|
|
61
|
+
op: "encodeDensity",
|
|
62
|
+
description: "Derive and encode a baseline-oriented kernel density area."
|
|
63
|
+
},
|
|
64
|
+
function (args = {}) {
|
|
65
|
+
validateOptions(args, OPTIONS, "encodeDensity");
|
|
66
|
+
const layer = findArea(this, args.target);
|
|
67
|
+
const field = requireField(args.field, "Density field");
|
|
68
|
+
const groupBy = args.groupBy === undefined
|
|
69
|
+
? undefined
|
|
70
|
+
: requireField(args.groupBy, "Density groupBy");
|
|
71
|
+
const source = validateUserId(
|
|
72
|
+
args.source ?? layer.data,
|
|
73
|
+
"Density source dataset id"
|
|
74
|
+
);
|
|
75
|
+
if (!hasDataset(this, source)) {
|
|
76
|
+
throw new Error(`Unknown density source dataset "${source}".`);
|
|
77
|
+
}
|
|
78
|
+
if (["x", "y", "y2", "group"].some(channel =>
|
|
79
|
+
layer.encoding?.[channel] !== undefined
|
|
80
|
+
)) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`Density area target "${layer.id}" already has positional or group encodings.`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
const densityChannel = args.densityChannel ?? "y";
|
|
86
|
+
if (!["x", "y"].includes(densityChannel)) {
|
|
87
|
+
throw new Error(`Unsupported densityChannel "${densityChannel}".`);
|
|
88
|
+
}
|
|
89
|
+
const output = args.as ?? [`${field}_value`, `${field}_density`];
|
|
90
|
+
if (!Array.isArray(output) || output.length !== 2) {
|
|
91
|
+
throw new TypeError("Density as must contain value and density field names.");
|
|
92
|
+
}
|
|
93
|
+
const derivedId = `${layer.id}DensityData`;
|
|
94
|
+
if (hasDataset(this, derivedId)) {
|
|
95
|
+
throw new Error(`Dataset "${derivedId}" already exists.`);
|
|
96
|
+
}
|
|
97
|
+
const valueScale = scaleOptions(
|
|
98
|
+
args.valueScale,
|
|
99
|
+
{ nice: false, zero: false },
|
|
100
|
+
"Density valueScale"
|
|
101
|
+
);
|
|
102
|
+
const densityScale = scaleOptions(
|
|
103
|
+
args.densityScale,
|
|
104
|
+
{ nice: true, zero: true },
|
|
105
|
+
"Density densityScale"
|
|
106
|
+
);
|
|
107
|
+
const xIsDensity = densityChannel === "x";
|
|
108
|
+
const xField = output[xIsDensity ? 1 : 0];
|
|
109
|
+
const yField = output[xIsDensity ? 0 : 1];
|
|
110
|
+
const xScale = xIsDensity ? densityScale : valueScale;
|
|
111
|
+
const yScale = xIsDensity ? valueScale : densityScale;
|
|
112
|
+
|
|
113
|
+
let next = this
|
|
114
|
+
.createDensityData({
|
|
115
|
+
id: derivedId,
|
|
116
|
+
source,
|
|
117
|
+
field,
|
|
118
|
+
...(groupBy === undefined ? {} : { groupBy }),
|
|
119
|
+
...(args.bandwidth === undefined ? {} : { bandwidth: args.bandwidth }),
|
|
120
|
+
...(args.extent === undefined ? {} : { extent: args.extent }),
|
|
121
|
+
...(args.steps === undefined ? {} : { steps: args.steps }),
|
|
122
|
+
...(args.kernel === undefined ? {} : { kernel: args.kernel }),
|
|
123
|
+
...(args.normalization === undefined
|
|
124
|
+
? {}
|
|
125
|
+
: { normalization: args.normalization }),
|
|
126
|
+
...(args.as === undefined ? {} : { as: args.as })
|
|
127
|
+
})
|
|
128
|
+
.editSemantic({
|
|
129
|
+
property: `layer[${layer.id}].data`,
|
|
130
|
+
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
|
+
});
|
|
144
|
+
if (groupBy !== undefined) {
|
|
145
|
+
next = next.encodeGroup({ target: layer.id, field: groupBy });
|
|
146
|
+
}
|
|
147
|
+
return next.rematerializeAreaMark({ id: layer.id });
|
|
148
|
+
}
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
function findDensityArea(program, requested) {
|
|
152
|
+
const target = requested === undefined
|
|
153
|
+
? undefined
|
|
154
|
+
: validateUserId(requested, "Density target id");
|
|
155
|
+
return resolveEligibleLayer(program, {
|
|
156
|
+
target,
|
|
157
|
+
label: "density area",
|
|
158
|
+
predicate(layer) {
|
|
159
|
+
const dataset = findDataset(program, layer.data);
|
|
160
|
+
return layer.mark?.type === "area" &&
|
|
161
|
+
dataset?.transform?.length === 1 &&
|
|
162
|
+
dataset.transform[0].type === "density";
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
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
|
+
const editDensity = action(
|
|
176
|
+
{
|
|
177
|
+
op: "editDensity",
|
|
178
|
+
description: "Revise one density transform and rematerialize its consumers."
|
|
179
|
+
},
|
|
180
|
+
function (args = {}) {
|
|
181
|
+
validateOptions(args, EDIT_OPTIONS, "editDensity");
|
|
182
|
+
if (!EDITABLE.some(option => Object.hasOwn(args, option))) {
|
|
183
|
+
throw new Error("editDensity requires at least one density option.");
|
|
184
|
+
}
|
|
185
|
+
const layer = findDensityArea(this, args.target);
|
|
186
|
+
const previous = findDataset(this, layer.data);
|
|
187
|
+
const transform = previous.transform[0];
|
|
188
|
+
const revisionId = nextDensityRevisionId(this, layer.id);
|
|
189
|
+
const option = property => Object.hasOwn(args, property)
|
|
190
|
+
? args[property]
|
|
191
|
+
: transform[property];
|
|
192
|
+
|
|
193
|
+
let next = this
|
|
194
|
+
.createDensityData({
|
|
195
|
+
id: revisionId,
|
|
196
|
+
source: previous.source,
|
|
197
|
+
field: transform.field,
|
|
198
|
+
...(transform.groupBy === undefined
|
|
199
|
+
? {}
|
|
200
|
+
: { groupBy: transform.groupBy }),
|
|
201
|
+
bandwidth: option("bandwidth"),
|
|
202
|
+
extent: option("extent"),
|
|
203
|
+
steps: option("steps"),
|
|
204
|
+
kernel: option("kernel") ?? "gaussian",
|
|
205
|
+
normalization: option("normalization") ?? "unit",
|
|
206
|
+
as: transform.as
|
|
207
|
+
})
|
|
208
|
+
.editSemantic({
|
|
209
|
+
property: `layer[${layer.id}].data`,
|
|
210
|
+
value: revisionId
|
|
211
|
+
})
|
|
212
|
+
.releaseDerivedData({ id: previous.id });
|
|
213
|
+
|
|
214
|
+
next = applyMaterializationPlan(
|
|
215
|
+
next,
|
|
216
|
+
planDensityRematerialization(next, layer.id)
|
|
217
|
+
);
|
|
218
|
+
return next;
|
|
219
|
+
}
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
export function registerDensityEncodingAction(ProgramClass) {
|
|
223
|
+
ProgramClass.prototype.encodeDensity = encodeDensity;
|
|
224
|
+
ProgramClass.prototype.editDensity = editDensity;
|
|
225
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import { validateOptions } from "./shared.js";
|
|
3
|
+
|
|
4
|
+
const HISTOGRAM_OPTIONS = Object.freeze([
|
|
5
|
+
"field", "target", "coordinate", "maxBins", "binStep", "binBoundaries",
|
|
6
|
+
"stack", "xScale", "yScale"
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
const encodeHistogram = action(
|
|
10
|
+
{
|
|
11
|
+
op: "encodeHistogram",
|
|
12
|
+
description: "Encode a binned count histogram with a configurable layout stack."
|
|
13
|
+
},
|
|
14
|
+
function (args = {}) {
|
|
15
|
+
validateOptions(args, HISTOGRAM_OPTIONS, "encodeHistogram");
|
|
16
|
+
const binOptions = ["maxBins", "binStep", "binBoundaries"].filter(
|
|
17
|
+
key => Object.hasOwn(args, key)
|
|
18
|
+
);
|
|
19
|
+
if (binOptions.length > 1) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
"encodeHistogram accepts only one of maxBins, binStep, or binBoundaries."
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
const bin = Object.hasOwn(args, "binStep")
|
|
25
|
+
? { step: args.binStep }
|
|
26
|
+
: Object.hasOwn(args, "binBoundaries")
|
|
27
|
+
? { boundaries: args.binBoundaries }
|
|
28
|
+
: { maxBins: args.maxBins ?? 10 };
|
|
29
|
+
const x = {
|
|
30
|
+
field: args.field,
|
|
31
|
+
bin
|
|
32
|
+
};
|
|
33
|
+
const y = {
|
|
34
|
+
stack: Object.hasOwn(args, "stack") ? args.stack : "zero"
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
for (const key of ["target", "coordinate"]) {
|
|
38
|
+
if (Object.hasOwn(args, key)) x[key] = args[key];
|
|
39
|
+
}
|
|
40
|
+
if (Object.hasOwn(args, "target")) y.target = args.target;
|
|
41
|
+
if (Object.hasOwn(args, "xScale")) x.scale = args.xScale;
|
|
42
|
+
if (Object.hasOwn(args, "yScale")) y.scale = args.yScale;
|
|
43
|
+
|
|
44
|
+
return this.encodeX(x).encodeY(y);
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
export function registerHistogramEncodingAction(ProgramClass) {
|
|
49
|
+
ProgramClass.prototype.encodeHistogram = encodeHistogram;
|
|
50
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { registerAppearanceEncodingAction } from "./appearance.js";
|
|
2
|
+
import { registerBarWidthEncodingAction } from "./barWidth.js";
|
|
3
|
+
import { registerColorEncodingAction } from "./color.js";
|
|
4
|
+
import { registerDensityEncodingAction } from "./density.js";
|
|
5
|
+
import { registerHistogramEncodingAction } from "./histogram.js";
|
|
6
|
+
import { registerOffsetEncodingAction } from "./offset.js";
|
|
7
|
+
import { registerPositionEncodingActions } from "./position.js";
|
|
8
|
+
import { registerRangedEncodingActions } from "./ranged.js";
|
|
9
|
+
import { registerStrokeDashEncodingActions } from "./strokeDash.js";
|
|
10
|
+
import { registerRuleAppearanceEncodingActions } from "./ruleAppearance.js";
|
|
11
|
+
|
|
12
|
+
export function registerEncodingActions(ProgramClass) {
|
|
13
|
+
registerPositionEncodingActions(ProgramClass);
|
|
14
|
+
registerRangedEncodingActions(ProgramClass);
|
|
15
|
+
registerOffsetEncodingAction(ProgramClass);
|
|
16
|
+
registerBarWidthEncodingAction(ProgramClass);
|
|
17
|
+
registerHistogramEncodingAction(ProgramClass);
|
|
18
|
+
registerDensityEncodingAction(ProgramClass);
|
|
19
|
+
registerColorEncodingAction(ProgramClass);
|
|
20
|
+
registerStrokeDashEncodingActions(ProgramClass);
|
|
21
|
+
registerAppearanceEncodingAction(ProgramClass);
|
|
22
|
+
registerRuleAppearanceEncodingActions(ProgramClass);
|
|
23
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import {
|
|
3
|
+
readNominalField,
|
|
4
|
+
validateNominalFieldType
|
|
5
|
+
} from "../../grammar/scales.js";
|
|
6
|
+
import { resolveOffsetScaleDefinition } from "../scales/definitions.js";
|
|
7
|
+
import {
|
|
8
|
+
applyEncodingScale,
|
|
9
|
+
resolveReassignmentScaleOptions,
|
|
10
|
+
resolveTarget,
|
|
11
|
+
validateOptions
|
|
12
|
+
} from "./shared.js";
|
|
13
|
+
import {
|
|
14
|
+
resolveBarColorLayout,
|
|
15
|
+
resolveBarGrain
|
|
16
|
+
} from "../../grammar/bars/policy.js";
|
|
17
|
+
import { normalizeOffsetPadding } from "../../grammar/bars/geometry.js";
|
|
18
|
+
import { applyMaterializationPlan } from "../../materialization/dependencies.js";
|
|
19
|
+
import { planEncodingRematerialization } from "../../materialization/encodings.js";
|
|
20
|
+
|
|
21
|
+
const ENCODING_OPTIONS = Object.freeze([
|
|
22
|
+
"field", "target", "fieldType", "scale", "paddingInner", "paddingOuter"
|
|
23
|
+
]);
|
|
24
|
+
const encodeXOffset = action(
|
|
25
|
+
{
|
|
26
|
+
op: "encodeXOffset",
|
|
27
|
+
description: "Encode a nominal field within each ordinal x band."
|
|
28
|
+
},
|
|
29
|
+
function (args = {}) {
|
|
30
|
+
validateOptions(args, ENCODING_OPTIONS, "encodeXOffset");
|
|
31
|
+
const fieldType = validateNominalFieldType(args.fieldType ?? "nominal");
|
|
32
|
+
const { id: target, dataset, layer } = resolveTarget(
|
|
33
|
+
this,
|
|
34
|
+
args.target,
|
|
35
|
+
["bar"],
|
|
36
|
+
"bar mark"
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
if (
|
|
40
|
+
resolveBarGrain(layer) === undefined ||
|
|
41
|
+
layer.encoding.x.scale === undefined
|
|
42
|
+
) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
"encodeXOffset requires a complete bar x/y encoding."
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
if (
|
|
48
|
+
layer.encoding?.color !== undefined &&
|
|
49
|
+
(resolveBarColorLayout(layer) !== "group" ||
|
|
50
|
+
layer.encoding.color.field !== args.field)
|
|
51
|
+
) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
"encodeXOffset field must match a grouped bar color field."
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
readNominalField(dataset.values, args.field);
|
|
58
|
+
const requestedScale = resolveReassignmentScaleOptions(
|
|
59
|
+
layer.encoding?.xOffset,
|
|
60
|
+
args.scale ?? {}
|
|
61
|
+
);
|
|
62
|
+
const scale = resolveOffsetScaleDefinition(this, requestedScale);
|
|
63
|
+
if (Object.hasOwn(scale, "unknown")) {
|
|
64
|
+
throw new Error("xOffset scale unknown is not supported for grouped bars.");
|
|
65
|
+
}
|
|
66
|
+
const padding = normalizeOffsetPadding(
|
|
67
|
+
args,
|
|
68
|
+
this.markConfigs[target]?.xOffset
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
let next = this
|
|
72
|
+
.editSemantic({
|
|
73
|
+
property: `layer[${target}].encoding.xOffset.field`,
|
|
74
|
+
value: args.field
|
|
75
|
+
})
|
|
76
|
+
.editSemantic({
|
|
77
|
+
property: `layer[${target}].encoding.xOffset.fieldType`,
|
|
78
|
+
value: fieldType
|
|
79
|
+
})
|
|
80
|
+
.editSemantic({
|
|
81
|
+
property: `layer[${target}].encoding.xOffset.scale`,
|
|
82
|
+
value: scale.id
|
|
83
|
+
})
|
|
84
|
+
._withMarkConfig(target, {
|
|
85
|
+
...this.markConfigs[target],
|
|
86
|
+
xOffset: padding
|
|
87
|
+
});
|
|
88
|
+
next = applyEncodingScale(next, scale, requestedScale, {
|
|
89
|
+
reassignment: layer.encoding?.xOffset?.scale === scale.id
|
|
90
|
+
});
|
|
91
|
+
return applyMaterializationPlan(
|
|
92
|
+
next,
|
|
93
|
+
planEncodingRematerialization(next, {
|
|
94
|
+
target,
|
|
95
|
+
channel: "xOffset",
|
|
96
|
+
scale: scale.id
|
|
97
|
+
})
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
export function registerOffsetEncodingAction(ProgramClass) {
|
|
103
|
+
ProgramClass.prototype.encodeXOffset = encodeXOffset;
|
|
104
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { validateStack, emptyPositionPolicy } from "./common.js";
|
|
2
|
+
|
|
3
|
+
export function resolveAreaPositionPolicy({ dataset, channel, args, fieldType }) {
|
|
4
|
+
if (!["quantitative", "temporal"].includes(fieldType)) {
|
|
5
|
+
throw new Error(
|
|
6
|
+
"Area position encoding requires quantitative or temporal fields."
|
|
7
|
+
);
|
|
8
|
+
}
|
|
9
|
+
if (args.aggregate !== undefined || args.bin !== undefined) {
|
|
10
|
+
throw new Error("Area position encoding does not support aggregate or bin.");
|
|
11
|
+
}
|
|
12
|
+
if (args.stack === undefined) return emptyPositionPolicy();
|
|
13
|
+
if (
|
|
14
|
+
channel !== "y" ||
|
|
15
|
+
!dataset.transform?.some(transform => transform.type === "density")
|
|
16
|
+
) {
|
|
17
|
+
throw new Error("Area stack currently requires a density y encoding.");
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
bin: undefined,
|
|
21
|
+
aggregate: undefined,
|
|
22
|
+
stack: validateStack(args.stack, "Area y encoding")
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import {
|
|
2
|
+
validateAggregate,
|
|
3
|
+
validateAggregateFieldType
|
|
4
|
+
} from "../../../../grammar/aggregate.js";
|
|
5
|
+
import {
|
|
6
|
+
BAR_ORIENTATIONS,
|
|
7
|
+
resolveBarOrientation
|
|
8
|
+
} from "../../../../grammar/bars/policy.js";
|
|
9
|
+
import { resolveBin, validateStack } from "./common.js";
|
|
10
|
+
|
|
11
|
+
export function resolveBarPositionPolicy({
|
|
12
|
+
program,
|
|
13
|
+
layer,
|
|
14
|
+
channel,
|
|
15
|
+
args,
|
|
16
|
+
fieldType,
|
|
17
|
+
field
|
|
18
|
+
}) {
|
|
19
|
+
let bin;
|
|
20
|
+
let aggregate;
|
|
21
|
+
let stack;
|
|
22
|
+
const xEncoding = layer.encoding?.x;
|
|
23
|
+
const opposite = layer.encoding?.[channel === "x" ? "y" : "x"];
|
|
24
|
+
const pendingBoxRange = program.markConfigs[layer.id]?.boxPlot !== undefined;
|
|
25
|
+
|
|
26
|
+
if (["nominal", "ordinal", "temporal"].includes(fieldType)) {
|
|
27
|
+
if (args.aggregate !== undefined || args.bin !== undefined || args.stack !== undefined) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
"Categorical bar position does not support bin or aggregate; a binned bar requires a quantitative field."
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
} else if (fieldType === "quantitative" && channel === "x" && args.bin !== undefined) {
|
|
33
|
+
if (args.aggregate !== undefined || args.stack !== undefined) {
|
|
34
|
+
throw new Error("Binned bar x encoding does not support aggregate or stack.");
|
|
35
|
+
}
|
|
36
|
+
bin = resolveBin(args.bin);
|
|
37
|
+
} else if (
|
|
38
|
+
fieldType === "quantitative" &&
|
|
39
|
+
channel === "y" &&
|
|
40
|
+
xEncoding?.bin !== undefined
|
|
41
|
+
) {
|
|
42
|
+
if (args.bin !== undefined) {
|
|
43
|
+
throw new Error("Histogram bar y encoding does not support bin.");
|
|
44
|
+
}
|
|
45
|
+
if (field !== xEncoding.field) {
|
|
46
|
+
throw new Error("Bar y field must match the binned x field.");
|
|
47
|
+
}
|
|
48
|
+
aggregate = args.aggregate ?? "count";
|
|
49
|
+
stack = Object.hasOwn(args, "stack") ? args.stack : "zero";
|
|
50
|
+
if (aggregate !== "count") {
|
|
51
|
+
throw new Error('Histogram bar y aggregate must be "count".');
|
|
52
|
+
}
|
|
53
|
+
stack = validateStack(stack, "Histogram bar y encoding");
|
|
54
|
+
} else if (fieldType === "quantitative") {
|
|
55
|
+
if (args.bin !== undefined) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
channel === "y"
|
|
58
|
+
? "Bar y does not support bin; histogram y requires a binned x encoding."
|
|
59
|
+
: "Quantitative bar measure encoding does not support bin."
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
aggregate = args.aggregate ?? (
|
|
63
|
+
["nominal", "ordinal", "temporal"].includes(opposite?.fieldType) &&
|
|
64
|
+
!pendingBoxRange
|
|
65
|
+
? "mean"
|
|
66
|
+
: undefined
|
|
67
|
+
);
|
|
68
|
+
if (pendingBoxRange && args.aggregate === undefined) {
|
|
69
|
+
return { bin, aggregate, stack };
|
|
70
|
+
}
|
|
71
|
+
if (aggregate === undefined) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
channel === "x"
|
|
74
|
+
? "Quantitative bar x encoding requires bin or aggregate."
|
|
75
|
+
: "Bar y encoding requires a binned quantitative or ordinal x category, temporal x category, or aggregate."
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
stack = Object.hasOwn(args, "stack") ? args.stack : null;
|
|
79
|
+
aggregate = validateAggregate(aggregate);
|
|
80
|
+
validateAggregateFieldType(aggregate, fieldType);
|
|
81
|
+
stack = validateStack(stack, `Bar ${channel} encoding`);
|
|
82
|
+
} else {
|
|
83
|
+
throw new Error(
|
|
84
|
+
"Bar position requires quantitative, temporal, ordinal, or nominal fields."
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const candidate = {
|
|
89
|
+
...layer,
|
|
90
|
+
encoding: {
|
|
91
|
+
...layer.encoding,
|
|
92
|
+
[channel]: { field, fieldType, bin, aggregate, stack }
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const orientation = resolveBarOrientation(candidate);
|
|
96
|
+
if (opposite !== undefined && orientation === undefined && !pendingBoxRange) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`Bar ${channel} encoding requires a quantitative field opposite a categorical position.`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
if (
|
|
102
|
+
orientation === BAR_ORIENTATIONS.horizontal &&
|
|
103
|
+
candidate.encoding?.xOffset !== undefined
|
|
104
|
+
) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
"Horizontal grouped bars require yOffset support, which is not available yet."
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
return { bin, aggregate, stack };
|
|
110
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { STACK_MODES } from "../../../../core/vocabulary.js";
|
|
2
|
+
import { normalizeHistogramBin } from "../../../../grammar/histogram.js";
|
|
3
|
+
|
|
4
|
+
export function resolveBin(bin) {
|
|
5
|
+
return normalizeHistogramBin(bin);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function validateStack(stack, label) {
|
|
9
|
+
if (stack !== null && !STACK_MODES.includes(stack)) {
|
|
10
|
+
throw new Error(`${label} has unsupported stack "${stack}".`);
|
|
11
|
+
}
|
|
12
|
+
return stack;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function emptyPositionPolicy() {
|
|
16
|
+
return { bin: undefined, aggregate: undefined, stack: undefined };
|
|
17
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { validatePositionFieldCompatibility } from
|
|
2
|
+
"../../../../grammar/positionCompatibility.js";
|
|
3
|
+
import { resolveAreaPositionPolicy } from "./area.js";
|
|
4
|
+
import { resolveBarPositionPolicy } from "./bar.js";
|
|
5
|
+
import { resolveLinePositionPolicy } from "./line.js";
|
|
6
|
+
import { resolvePointPositionPolicy } from "./point.js";
|
|
7
|
+
import { resolveRulePositionPolicy } from "./rule.js";
|
|
8
|
+
|
|
9
|
+
const POSITION_POLICIES = Object.freeze({
|
|
10
|
+
area: resolveAreaPositionPolicy,
|
|
11
|
+
bar: resolveBarPositionPolicy,
|
|
12
|
+
line: resolveLinePositionPolicy,
|
|
13
|
+
point: resolvePointPositionPolicy,
|
|
14
|
+
rule: resolveRulePositionPolicy
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export function resolveMarkPositionPolicy(context) {
|
|
18
|
+
const { layer, channel, fieldType } = context;
|
|
19
|
+
validatePositionFieldCompatibility(layer.mark.type, channel, fieldType);
|
|
20
|
+
const policy = POSITION_POLICIES[layer.mark.type];
|
|
21
|
+
if (policy === undefined) {
|
|
22
|
+
throw new Error(`Unsupported position policy for mark "${layer.mark.type}".`);
|
|
23
|
+
}
|
|
24
|
+
return policy(context);
|
|
25
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {
|
|
2
|
+
validateAggregate,
|
|
3
|
+
validateAggregateFieldType
|
|
4
|
+
} from "../../../../grammar/aggregate.js";
|
|
5
|
+
import { emptyPositionPolicy } from "./common.js";
|
|
6
|
+
|
|
7
|
+
function rejectSharedOptions(args, channel) {
|
|
8
|
+
if (args.bin !== undefined) {
|
|
9
|
+
throw new Error(`Line ${channel} encoding does not support bin.`);
|
|
10
|
+
}
|
|
11
|
+
if (args.stack !== undefined) {
|
|
12
|
+
throw new Error(`Line ${channel} encoding does not support stack.`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function resolveLinePositionPolicy({
|
|
17
|
+
layer,
|
|
18
|
+
dataset,
|
|
19
|
+
channel,
|
|
20
|
+
args,
|
|
21
|
+
fieldType
|
|
22
|
+
}) {
|
|
23
|
+
const regression = dataset.transform?.some(item => item.type === "regression");
|
|
24
|
+
const interval = dataset.transform?.some(item => item.type === "interval");
|
|
25
|
+
|
|
26
|
+
if (channel === "x") {
|
|
27
|
+
const horizontalDirect =
|
|
28
|
+
layer.encoding?.y?.fieldType === "temporal" &&
|
|
29
|
+
fieldType === "quantitative";
|
|
30
|
+
if (
|
|
31
|
+
fieldType !== "temporal" &&
|
|
32
|
+
!((regression || interval || horizontalDirect) && fieldType === "quantitative")
|
|
33
|
+
) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
"Line x encoding requires a temporal field or a compatible derived quantitative field."
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
if (args.aggregate !== undefined) {
|
|
39
|
+
throw new Error("Line x encoding does not support aggregate.");
|
|
40
|
+
}
|
|
41
|
+
rejectSharedOptions(args, "x");
|
|
42
|
+
return emptyPositionPolicy();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
rejectSharedOptions(args, "y");
|
|
46
|
+
const prospectiveDirect =
|
|
47
|
+
args.aggregate === undefined &&
|
|
48
|
+
(fieldType === "temporal" ||
|
|
49
|
+
(fieldType === "quantitative" && layer.encoding?.x === undefined));
|
|
50
|
+
if (interval || prospectiveDirect) {
|
|
51
|
+
if (!["quantitative", "temporal"].includes(fieldType)) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
"Direct line y encoding requires a quantitative or temporal field."
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
if (args.aggregate !== undefined) {
|
|
57
|
+
throw new Error("Direct line y encoding does not support aggregate.");
|
|
58
|
+
}
|
|
59
|
+
return emptyPositionPolicy();
|
|
60
|
+
}
|
|
61
|
+
if (regression) {
|
|
62
|
+
if (fieldType !== "quantitative") {
|
|
63
|
+
throw new Error("Regression line y encoding requires a quantitative field.");
|
|
64
|
+
}
|
|
65
|
+
if (args.aggregate !== undefined) {
|
|
66
|
+
throw new Error("Regression line y encoding does not support aggregate.");
|
|
67
|
+
}
|
|
68
|
+
return emptyPositionPolicy();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const aggregate = validateAggregate(args.aggregate);
|
|
72
|
+
validateAggregateFieldType(aggregate, fieldType);
|
|
73
|
+
return { bin: undefined, aggregate, stack: undefined };
|
|
74
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { emptyPositionPolicy } from "./common.js";
|
|
2
|
+
|
|
3
|
+
export function resolvePointPositionPolicy({ args, fieldType }) {
|
|
4
|
+
if (!["quantitative", "temporal", "ordinal", "nominal"].includes(fieldType)) {
|
|
5
|
+
throw new Error(
|
|
6
|
+
"Point position encoding requires quantitative, temporal, ordinal, or nominal fields."
|
|
7
|
+
);
|
|
8
|
+
}
|
|
9
|
+
if (args.aggregate !== undefined) {
|
|
10
|
+
throw new Error("Point position encoding does not support aggregate.");
|
|
11
|
+
}
|
|
12
|
+
if (args.bin !== undefined) {
|
|
13
|
+
throw new Error("Point position encoding does not support bin.");
|
|
14
|
+
}
|
|
15
|
+
if (args.stack !== undefined) {
|
|
16
|
+
throw new Error("Point position encoding does not support stack.");
|
|
17
|
+
}
|
|
18
|
+
return emptyPositionPolicy();
|
|
19
|
+
}
|