ggaction 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -0
- package/README.md +35 -13
- package/package.json +18 -7
- package/src/actions/boxPlots/create.js +13 -7
- package/src/actions/boxPlots/edit.js +484 -36
- package/src/actions/boxPlots/materialize.js +3 -0
- package/src/actions/boxPlots/options.js +11 -4
- package/src/actions/boxPlots/resolve.js +10 -2
- package/src/actions/charts/bar.js +50 -0
- package/src/actions/charts/heatmap.js +205 -0
- package/src/actions/charts/histogram.js +58 -0
- package/src/actions/charts/index.js +15 -0
- package/src/actions/charts/line.js +69 -0
- package/src/actions/charts/parallel.js +76 -0
- package/src/actions/charts/scatter.js +56 -0
- package/src/actions/charts/shared.js +121 -0
- package/src/actions/composition/actions.js +13 -3
- package/src/actions/coordinates/parallel.js +50 -0
- package/src/actions/data/bin2d.js +260 -0
- package/src/actions/data/density.js +54 -13
- package/src/actions/data/derived.js +22 -0
- package/src/actions/data/filter.js +5 -0
- package/src/actions/data/gradientProfile.js +65 -0
- package/src/actions/data/horizon.js +71 -0
- package/src/actions/data/index.js +34 -2
- package/src/actions/data/intervalEdit.js +76 -0
- package/src/actions/data/window.js +50 -0
- package/src/actions/distributions/revision.js +326 -0
- package/src/actions/encodings/appearance.js +35 -1
- package/src/actions/encodings/color/index.js +17 -1
- package/src/actions/encodings/color/layout.js +8 -0
- package/src/actions/encodings/density/resolve.js +109 -0
- package/src/actions/encodings/density.js +330 -90
- package/src/actions/encodings/horizon/resolve.js +283 -0
- package/src/actions/encodings/horizon.js +364 -0
- package/src/actions/encodings/index.js +8 -0
- package/src/actions/encodings/parallel.js +115 -0
- package/src/actions/encodings/pathOrder.js +131 -0
- package/src/actions/encodings/position/apply.js +73 -4
- package/src/actions/encodings/position/index.js +15 -2
- package/src/actions/encodings/position/policies/arc.js +27 -4
- package/src/actions/encodings/position/policies/area.js +33 -2
- package/src/actions/encodings/position/policies/index.js +7 -2
- package/src/actions/encodings/position/policies/line.js +18 -2
- package/src/actions/encodings/position/resolve.js +24 -2
- package/src/actions/encodings/ranged.js +3 -1
- package/src/actions/encodings/remove.js +268 -0
- package/src/actions/encodings/ruleAppearance.js +88 -10
- package/src/actions/errorBands/edit.js +121 -13
- package/src/actions/errorBars/edit.js +45 -8
- package/src/actions/errorBars/options.js +7 -1
- package/src/actions/facets/actions.js +137 -3
- package/src/actions/facets/derive.js +33 -3
- package/src/actions/facets/replay.js +7 -36
- package/src/actions/gradientPlots/components.js +177 -0
- package/src/actions/gradientPlots/create.js +96 -0
- package/src/actions/gradientPlots/edit.js +501 -0
- package/src/actions/gradientPlots/index.js +24 -0
- package/src/actions/gradientPlots/materialize.js +287 -0
- package/src/actions/gradientPlots/options.js +120 -0
- package/src/actions/gradientPlots/paint.js +107 -0
- package/src/actions/gradientPlots/rebind.js +51 -0
- package/src/actions/gradientPlots/resolve.js +81 -0
- package/src/actions/guides/applicability.js +44 -17
- package/src/actions/guides/axes/axes.js +37 -3
- package/src/actions/guides/axes/edit.js +129 -47
- package/src/actions/guides/axes/index.js +2 -0
- package/src/actions/guides/axes/labels.js +11 -6
- package/src/actions/guides/axes/parallel/resolve.js +86 -0
- package/src/actions/guides/axes/parallel.js +228 -0
- package/src/actions/guides/axes/titles.js +11 -8
- package/src/actions/guides/guides.js +60 -5
- package/src/actions/guides/legends/categorical/actions.js +67 -19
- package/src/actions/guides/legends/continuous/common.js +3 -3
- package/src/actions/guides/legends/edit.js +63 -0
- package/src/actions/guides/legends/index.js +2 -0
- package/src/actions/guides/legends/remove.js +124 -30
- package/src/actions/guides/legends/strokeWidth.js +198 -0
- package/src/actions/index.js +6 -0
- package/src/actions/marks/arc/actions.js +46 -14
- package/src/actions/marks/area/actions.js +60 -136
- package/src/actions/marks/area/materialize.js +163 -0
- package/src/actions/marks/index.js +5 -1
- package/src/actions/marks/line/actions.js +99 -80
- package/src/actions/marks/line/materialize.js +146 -0
- package/src/actions/marks/point/create.js +76 -0
- package/src/actions/marks/point/edit.js +97 -0
- package/src/actions/marks/point/index.js +11 -1
- package/src/actions/marks/point/jitter.js +72 -0
- package/src/actions/marks/point/{actions.js → materialize.js} +183 -172
- package/src/actions/marks/rect/actions.js +3 -0
- package/src/actions/marks/remove.js +13 -2
- package/src/actions/marks/rule/actions.js +29 -4
- package/src/actions/marks/text/actions.js +19 -4
- package/src/actions/marks/text/index.js +7 -1
- package/src/actions/marks/text/layout.js +321 -0
- package/src/actions/primitives/semanticValidation/dataset.js +22 -0
- package/src/actions/primitives/semanticValidation/guide.js +43 -0
- package/src/actions/primitives/semanticValidation/index.js +23 -0
- package/src/actions/primitives/semanticValidation/layer.js +74 -0
- package/src/actions/primitives/semanticValidation/scale.js +74 -0
- package/src/actions/primitives/semanticValidation/shared.js +5 -0
- package/src/actions/primitives/semanticValue.js +1 -204
- package/src/actions/regression/edit.js +247 -88
- package/src/actions/scales/consumers/common.js +18 -0
- package/src/actions/scales/consumers/families.js +14 -3
- package/src/actions/scales/definitions.js +30 -0
- package/src/actions/scales/edit.js +6 -204
- package/src/actions/scales/editPolicy.js +214 -0
- package/src/actions/scales/materialize.js +1 -1
- package/src/actions/scales/patch.js +26 -0
- package/src/actions/selection/actions.js +150 -5
- package/src/actions/selection/index.js +6 -0
- package/src/actions/violinPlots/create.js +198 -0
- package/src/actions/violinPlots/index.js +5 -0
- package/src/core/textMetrics.js +47 -0
- package/src/core/vocabulary.js +14 -2
- package/src/grammar/arcs.js +49 -12
- package/src/grammar/areaSeries.js +41 -5
- package/src/grammar/bin2d.js +333 -0
- package/src/grammar/categoricalDensity.js +192 -0
- package/src/grammar/coordinates.js +1 -1
- package/src/grammar/curveCommands.js +3 -1
- package/src/grammar/density.js +252 -26
- package/src/grammar/facets/dependencies.js +3 -11
- package/src/grammar/facets/index.js +3 -1
- package/src/grammar/gradientProfile.js +213 -0
- package/src/grammar/horizon.js +455 -0
- package/src/grammar/jitter.js +197 -0
- package/src/grammar/lineSeries.js +69 -0
- package/src/grammar/paint.js +88 -0
- package/src/grammar/parallelCoordinates.js +213 -0
- package/src/grammar/pathOrder.js +35 -0
- package/src/grammar/pointShapes.js +24 -0
- package/src/grammar/rules.js +1 -0
- package/src/grammar/scales/appearance.js +21 -0
- package/src/grammar/schemas/concreteGraphic.js +11 -1
- package/src/grammar/schemas/graphicBounds.js +19 -37
- package/src/grammar/schemas/semanticPath.js +17 -2
- package/src/grammar/transforms.js +100 -11
- package/src/grammar/window.js +339 -0
- package/src/layout/labels.js +270 -0
- package/src/layout/text.js +4 -1
- package/src/layout/title.js +10 -20
- package/src/materialization/dataProvenance.js +56 -2
- package/src/materialization/dependencies.js +9 -14
- package/src/materialization/guides/resources.js +74 -24
- package/src/materialization/horizon.js +29 -0
- package/src/materialization/layout.js +15 -0
- package/src/materialization/marks/capabilities.js +21 -1
- package/src/materialization/marks/index.js +3 -0
- package/src/materialization/marks/pathOrder.js +71 -0
- package/src/materialization/marks/policies.js +6 -2
- package/src/materialization/scaleGuideDependencies.js +8 -0
- package/src/materialization/scales/resolve.js +2 -0
- package/src/materialization/selection/items/path.js +27 -1
- package/src/materialization/selection/items/point.js +1 -5
- package/src/materialization/selection/items/rect.js +21 -0
- package/src/materialization/selection/styles.js +20 -9
- package/src/materialization/text.js +12 -4
- package/src/renderers/canvas/fill.js +33 -0
- package/src/renderers/canvas/index.js +7 -0
- package/src/renderers/canvas/path.js +7 -4
- package/src/renderers/canvas/rect.js +9 -3
- package/src/selectors/datasets.js +4 -0
- package/src/theme/defaults.js +4 -0
- package/types/extension.d.ts +6 -0
- package/types/index.d.ts +77 -0
- package/types/program.d.ts +703 -17
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { action } from "../../../core/action.js";
|
|
2
|
+
import { validateUserId } from "../../../core/identifiers.js";
|
|
3
|
+
import { validateOptionObject } from "../../../core/validation.js";
|
|
4
|
+
import { DEFAULT_TEXT_MARK } from "../../../grammar/text.js";
|
|
5
|
+
import { findGraphicParent } from "../../../grammar/schemas/graphicTree.js";
|
|
6
|
+
import { validateConcreteGraphicValue } from
|
|
7
|
+
"../../../grammar/schemas/concreteGraphic.js";
|
|
8
|
+
import {
|
|
9
|
+
normalizeLabelLayoutGeometry,
|
|
10
|
+
resolveLabelLayout,
|
|
11
|
+
resolveLabelLeader
|
|
12
|
+
} from "../../../layout/labels.js";
|
|
13
|
+
import { resolveGraphicBounds } from "../../../layout/canvas.js";
|
|
14
|
+
import { canMaterializeText } from "../../../materialization/marks/index.js";
|
|
15
|
+
import { resolveTextGraphicEntries } from "../../../materialization/text.js";
|
|
16
|
+
import { findLayer, resolveEligibleLayer } from "../../../selectors/layers.js";
|
|
17
|
+
|
|
18
|
+
const LAYOUT_OPTIONS = Object.freeze([
|
|
19
|
+
"target", "axis", "padding", "maxDisplacement", "bounds", "leader"
|
|
20
|
+
]);
|
|
21
|
+
const REMOVE_OPTIONS = Object.freeze(["target"]);
|
|
22
|
+
const MATERIALIZE_OPTIONS = Object.freeze(["id", "rematerializeBase"]);
|
|
23
|
+
const LEADER_OPTIONS = Object.freeze([
|
|
24
|
+
"stroke", "strokeWidth", "strokeDash", "opacity"
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
function normalizeLeader(value) {
|
|
28
|
+
if (value === false || value === undefined) return false;
|
|
29
|
+
validateOptionObject(value, LEADER_OPTIONS, "label leader");
|
|
30
|
+
const normalized = {
|
|
31
|
+
stroke: "#94a3b8",
|
|
32
|
+
strokeWidth: 1,
|
|
33
|
+
strokeDash: [],
|
|
34
|
+
opacity: 1,
|
|
35
|
+
...value
|
|
36
|
+
};
|
|
37
|
+
for (const [property, propertyValue] of Object.entries(normalized)) {
|
|
38
|
+
validateConcreteGraphicValue("line", property, propertyValue);
|
|
39
|
+
}
|
|
40
|
+
return normalized;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizePolicy(args) {
|
|
44
|
+
const geometry = normalizeLabelLayoutGeometry(Object.fromEntries(
|
|
45
|
+
Object.entries(args).filter(
|
|
46
|
+
([key]) => key !== "target" && key !== "leader"
|
|
47
|
+
)
|
|
48
|
+
));
|
|
49
|
+
return { ...geometry, leader: normalizeLeader(args.leader) };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function leaderId(target) {
|
|
53
|
+
return `${target}-label-leaders`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function requireCompleteText(program, requested, operation) {
|
|
57
|
+
const target = requested === undefined
|
|
58
|
+
? undefined
|
|
59
|
+
: validateUserId(requested, "Text mark id");
|
|
60
|
+
if (target !== undefined) {
|
|
61
|
+
const layer = findLayer(program, target);
|
|
62
|
+
if (layer?.mark?.type !== "text") {
|
|
63
|
+
throw new Error(`Unknown ${operation} text mark target "${target}".`);
|
|
64
|
+
}
|
|
65
|
+
if (!canMaterializeText(program, layer)) {
|
|
66
|
+
throw new Error(`${operation} requires a complete text mark.`);
|
|
67
|
+
}
|
|
68
|
+
return layer;
|
|
69
|
+
}
|
|
70
|
+
return resolveEligibleLayer(program, {
|
|
71
|
+
predicate: layer =>
|
|
72
|
+
layer.mark?.type === "text" && canMaterializeText(program, layer),
|
|
73
|
+
label: `${operation} text mark`
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function requireConfiguredText(program, requested) {
|
|
78
|
+
const target = requested === undefined
|
|
79
|
+
? undefined
|
|
80
|
+
: validateUserId(requested, "Text mark id");
|
|
81
|
+
return resolveEligibleLayer(program, {
|
|
82
|
+
target,
|
|
83
|
+
predicate: layer =>
|
|
84
|
+
layer.mark?.type === "text" &&
|
|
85
|
+
program.materializationConfigs.labelLayouts?.[layer.id] !== undefined,
|
|
86
|
+
label: "removeLabelLayout text mark"
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function concreteBounds(program, kind) {
|
|
91
|
+
if (kind === "plot") {
|
|
92
|
+
const plot = resolveGraphicBounds(program);
|
|
93
|
+
return {
|
|
94
|
+
left: plot.x,
|
|
95
|
+
right: plot.x + plot.width,
|
|
96
|
+
top: plot.y,
|
|
97
|
+
bottom: plot.y + plot.height
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const canvas = program.graphicSpec.objects.canvas;
|
|
101
|
+
if (
|
|
102
|
+
canvas?.type !== "canvas" ||
|
|
103
|
+
!Number.isFinite(canvas.properties.width) ||
|
|
104
|
+
!Number.isFinite(canvas.properties.height)
|
|
105
|
+
) {
|
|
106
|
+
throw new Error("Canvas label bounds require concrete Canvas dimensions.");
|
|
107
|
+
}
|
|
108
|
+
const x = canvas.properties.x ?? 0;
|
|
109
|
+
const y = canvas.properties.y ?? 0;
|
|
110
|
+
return {
|
|
111
|
+
left: x,
|
|
112
|
+
right: x + canvas.properties.width,
|
|
113
|
+
top: y,
|
|
114
|
+
bottom: y + canvas.properties.height
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function layoutInputs(program, layer) {
|
|
119
|
+
const entries = resolveTextGraphicEntries(
|
|
120
|
+
program,
|
|
121
|
+
layer,
|
|
122
|
+
program.markConfigs[layer.id] ?? DEFAULT_TEXT_MARK
|
|
123
|
+
);
|
|
124
|
+
const graphic = program.graphicSpec.objects[layer.id];
|
|
125
|
+
if (graphic?.type !== "text" || !Array.isArray(graphic.items)) {
|
|
126
|
+
throw new Error(`Text mark "${layer.id}" requires text collection graphics.`);
|
|
127
|
+
}
|
|
128
|
+
if (entries.length !== graphic.items.length) {
|
|
129
|
+
throw new Error(`Text mark "${layer.id}" base materialization is inconsistent.`);
|
|
130
|
+
}
|
|
131
|
+
return entries.map((entry, index) => ({
|
|
132
|
+
id: graphic.items[index].id,
|
|
133
|
+
x: entry.graphic.properties.x,
|
|
134
|
+
y: entry.graphic.properties.y,
|
|
135
|
+
sourceX: entry.anchor.x,
|
|
136
|
+
sourceY: entry.anchor.y,
|
|
137
|
+
text: entry.graphic.properties.text,
|
|
138
|
+
fontSize: entry.graphic.properties.fontSize,
|
|
139
|
+
textAlign: entry.graphic.properties.textAlign,
|
|
140
|
+
textBaseline: entry.graphic.properties.textBaseline,
|
|
141
|
+
rotation: entry.graphic.properties.rotation
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function leaderPlacement(program, layer) {
|
|
146
|
+
const targetParent = findGraphicParent(program.graphicSpec, layer.id);
|
|
147
|
+
if (targetParent?.kind !== "parent") {
|
|
148
|
+
throw new Error("Label leaders require the text graphic to have a parent container.");
|
|
149
|
+
}
|
|
150
|
+
const source = layer.source === undefined
|
|
151
|
+
? undefined
|
|
152
|
+
: program.graphicSpec.objects[layer.source];
|
|
153
|
+
const sourceParent = source === undefined
|
|
154
|
+
? undefined
|
|
155
|
+
: findGraphicParent(program.graphicSpec, layer.source);
|
|
156
|
+
const before = sourceParent?.kind === "parent" && sourceParent.id === targetParent.id
|
|
157
|
+
? layer.source
|
|
158
|
+
: layer.id;
|
|
159
|
+
return { parent: targetParent.id, before };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function removeLeaderGraphic(program, id) {
|
|
163
|
+
return program.graphicSpec.objects[id] === undefined
|
|
164
|
+
? program
|
|
165
|
+
: program.editGraphics({ target: id, remove: true });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function materializeLeaders(program, layer, config, resolved) {
|
|
169
|
+
let next = removeLeaderGraphic(program, config.leaderId);
|
|
170
|
+
if (config.leader === false) return { program: next, count: 0 };
|
|
171
|
+
const leaders = resolved.items.map(resolveLabelLeader).filter(Boolean);
|
|
172
|
+
if (leaders.length === 0) return { program: next, count: 0 };
|
|
173
|
+
next = next
|
|
174
|
+
.createGraphics({
|
|
175
|
+
id: config.leaderId,
|
|
176
|
+
type: "line",
|
|
177
|
+
length: leaders.length,
|
|
178
|
+
...leaderPlacement(next, layer)
|
|
179
|
+
})
|
|
180
|
+
.editGraphics({
|
|
181
|
+
target: config.leaderId,
|
|
182
|
+
property: "x1",
|
|
183
|
+
value: leaders.map(line => line.x1)
|
|
184
|
+
})
|
|
185
|
+
.editGraphics({
|
|
186
|
+
target: config.leaderId,
|
|
187
|
+
property: "y1",
|
|
188
|
+
value: leaders.map(line => line.y1)
|
|
189
|
+
})
|
|
190
|
+
.editGraphics({
|
|
191
|
+
target: config.leaderId,
|
|
192
|
+
property: "x2",
|
|
193
|
+
value: leaders.map(line => line.x2)
|
|
194
|
+
})
|
|
195
|
+
.editGraphics({
|
|
196
|
+
target: config.leaderId,
|
|
197
|
+
property: "y2",
|
|
198
|
+
value: leaders.map(line => line.y2)
|
|
199
|
+
});
|
|
200
|
+
for (const property of ["stroke", "strokeWidth", "strokeDash", "opacity"]) {
|
|
201
|
+
next = next.editGraphics({
|
|
202
|
+
target: config.leaderId,
|
|
203
|
+
property,
|
|
204
|
+
value: property === "strokeDash"
|
|
205
|
+
? leaders.map(() => config.leader.strokeDash)
|
|
206
|
+
: config.leader[property]
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
return { program: next, count: leaders.length };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const materializeLabelLayout = action(
|
|
213
|
+
{
|
|
214
|
+
op: "materializeLabelLayout",
|
|
215
|
+
description: "Resolve concrete label positions and optional leader lines."
|
|
216
|
+
},
|
|
217
|
+
function (args = {}) {
|
|
218
|
+
validateOptionObject(args, MATERIALIZE_OPTIONS, "materializeLabelLayout");
|
|
219
|
+
const id = validateUserId(args.id, "Text mark id");
|
|
220
|
+
const rematerializeBase = args.rematerializeBase ?? true;
|
|
221
|
+
if (typeof rematerializeBase !== "boolean") {
|
|
222
|
+
throw new TypeError("materializeLabelLayout rematerializeBase must be a boolean.");
|
|
223
|
+
}
|
|
224
|
+
const layer = findLayer(this, id);
|
|
225
|
+
if (layer?.mark?.type !== "text") throw new Error(`Unknown text mark "${id}".`);
|
|
226
|
+
const config = this.materializationConfigs.labelLayouts?.[id];
|
|
227
|
+
if (config === undefined) {
|
|
228
|
+
throw new Error(`Text mark "${id}" has no label layout policy.`);
|
|
229
|
+
}
|
|
230
|
+
let next = rematerializeBase
|
|
231
|
+
? this.rematerializeTextMark({ id, replayLayout: false })
|
|
232
|
+
: this;
|
|
233
|
+
const resolved = resolveLabelLayout({
|
|
234
|
+
items: canMaterializeText(next, layer) ? layoutInputs(next, layer) : [],
|
|
235
|
+
bounds: concreteBounds(next, config.bounds),
|
|
236
|
+
axis: config.axis,
|
|
237
|
+
padding: config.padding,
|
|
238
|
+
maxDisplacement: config.maxDisplacement
|
|
239
|
+
});
|
|
240
|
+
next = next
|
|
241
|
+
.editGraphics({
|
|
242
|
+
target: id,
|
|
243
|
+
property: "x",
|
|
244
|
+
value: resolved.items.map(item => item.x)
|
|
245
|
+
})
|
|
246
|
+
.editGraphics({
|
|
247
|
+
target: id,
|
|
248
|
+
property: "y",
|
|
249
|
+
value: resolved.items.map(item => item.y)
|
|
250
|
+
});
|
|
251
|
+
const leaders = materializeLeaders(next, layer, config, resolved);
|
|
252
|
+
next = leaders.program;
|
|
253
|
+
return next._withMaterializationConfig(["labelLayouts", id], {
|
|
254
|
+
axis: config.axis,
|
|
255
|
+
padding: config.padding,
|
|
256
|
+
maxDisplacement: config.maxDisplacement,
|
|
257
|
+
bounds: config.bounds,
|
|
258
|
+
leader: config.leader,
|
|
259
|
+
leaderId: config.leaderId,
|
|
260
|
+
resolution: {
|
|
261
|
+
overlapBefore: resolved.overlapBefore,
|
|
262
|
+
overlapAfter: resolved.overlapAfter,
|
|
263
|
+
displaced: resolved.items.filter(item => item.distance > 0).length,
|
|
264
|
+
leaders: leaders.count,
|
|
265
|
+
maximumDisplacement: resolved.items.reduce(
|
|
266
|
+
(maximum, item) => Math.max(maximum, item.distance),
|
|
267
|
+
0
|
|
268
|
+
),
|
|
269
|
+
warnings: resolved.warnings
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
const layoutLabels = action(
|
|
276
|
+
{
|
|
277
|
+
op: "layoutLabels",
|
|
278
|
+
description: "Assign deterministic collision-aware layout to a text mark."
|
|
279
|
+
},
|
|
280
|
+
function (args = {}) {
|
|
281
|
+
validateOptionObject(args, LAYOUT_OPTIONS, "layoutLabels");
|
|
282
|
+
const layer = requireCompleteText(this, args.target, "layoutLabels");
|
|
283
|
+
const policy = normalizePolicy(args);
|
|
284
|
+
const generatedId = leaderId(layer.id);
|
|
285
|
+
const existingConfig = this.materializationConfigs.labelLayouts?.[layer.id];
|
|
286
|
+
if (
|
|
287
|
+
this.graphicSpec.objects[generatedId] !== undefined &&
|
|
288
|
+
existingConfig?.leaderId !== generatedId
|
|
289
|
+
) {
|
|
290
|
+
throw new Error(`Label layout leader graphic "${generatedId}" already exists.`);
|
|
291
|
+
}
|
|
292
|
+
return this
|
|
293
|
+
._withMaterializationConfig(["labelLayouts", layer.id], {
|
|
294
|
+
...policy,
|
|
295
|
+
leaderId: generatedId
|
|
296
|
+
})
|
|
297
|
+
.materializeLabelLayout({ id: layer.id });
|
|
298
|
+
}
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
const removeLabelLayout = action(
|
|
302
|
+
{
|
|
303
|
+
op: "removeLabelLayout",
|
|
304
|
+
description: "Remove label layout and restore semantic base text positions."
|
|
305
|
+
},
|
|
306
|
+
function (args = {}) {
|
|
307
|
+
validateOptionObject(args, REMOVE_OPTIONS, "removeLabelLayout");
|
|
308
|
+
const layer = requireConfiguredText(this, args.target);
|
|
309
|
+
const config = this.materializationConfigs.labelLayouts[layer.id];
|
|
310
|
+
return removeLeaderGraphic(
|
|
311
|
+
this._withoutMaterializationConfig(["labelLayouts", layer.id]),
|
|
312
|
+
config.leaderId
|
|
313
|
+
).rematerializeTextMark({ id: layer.id });
|
|
314
|
+
}
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
export function registerTextLabelLayoutActions(ProgramClass) {
|
|
318
|
+
ProgramClass.prototype.layoutLabels = layoutLabels;
|
|
319
|
+
ProgramClass.prototype.removeLabelLayout = removeLabelLayout;
|
|
320
|
+
ProgramClass.prototype.materializeLabelLayout = materializeLabelLayout;
|
|
321
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { validateUserId } from "../../../core/identifiers.js";
|
|
2
|
+
import { isPlainObject } from "../../../core/immutable.js";
|
|
3
|
+
import { validateDatasetTransforms } from "../../../grammar/transforms.js";
|
|
4
|
+
import { hasDataset } from "../../../selectors/datasets.js";
|
|
5
|
+
|
|
6
|
+
export function validateDatasetSemanticValue(program, parsed, value) {
|
|
7
|
+
const property = parsed.path[0];
|
|
8
|
+
if (property === "values") {
|
|
9
|
+
if (!Array.isArray(value) || !value.every(isPlainObject)) {
|
|
10
|
+
throw new TypeError("Dataset values must be an array of plain row objects.");
|
|
11
|
+
}
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (property === "source") {
|
|
15
|
+
validateUserId(value, "Dataset source id");
|
|
16
|
+
if (!hasDataset(program, value)) {
|
|
17
|
+
throw new Error(`Unknown source dataset "${value}".`);
|
|
18
|
+
}
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (property === "transform") validateDatasetTransforms(value);
|
|
22
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { validateUserId } from "../../../core/identifiers.js";
|
|
2
|
+
import { CATEGORICAL_LEGEND_CHANNELS } from "../../../core/vocabulary.js";
|
|
3
|
+
import { validateNonEmptySemanticString } from "./shared.js";
|
|
4
|
+
|
|
5
|
+
function validateLegend(property, value) {
|
|
6
|
+
if (property === "title") {
|
|
7
|
+
validateNonEmptySemanticString(value, "Legend title");
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
11
|
+
throw new TypeError(`Legend ${property} must be a non-empty array.`);
|
|
12
|
+
}
|
|
13
|
+
if (new Set(value).size !== value.length) {
|
|
14
|
+
throw new Error(`Legend ${property} must not contain duplicates.`);
|
|
15
|
+
}
|
|
16
|
+
if (property === "channels") {
|
|
17
|
+
if (!value.every(channel => CATEGORICAL_LEGEND_CHANNELS.includes(channel))) {
|
|
18
|
+
throw new Error("Legend channels support only color, strokeDash, and shape.");
|
|
19
|
+
}
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
for (const id of value) validateUserId(id, "Legend scale id");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function validateGuideSemanticValue(_program, parsed, value) {
|
|
26
|
+
const property = parsed.path.at(-1);
|
|
27
|
+
if (parsed.id === "legend.series") {
|
|
28
|
+
validateLegend(property, value);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (!parsed.id.startsWith("grid.") && !parsed.id.startsWith("axis.")) return;
|
|
32
|
+
if (parsed.id === "axis.parallel" && property === "scales") {
|
|
33
|
+
if (!Array.isArray(value) || value.length < 2) {
|
|
34
|
+
throw new TypeError("Parallel axis scales must contain at least two ids.");
|
|
35
|
+
}
|
|
36
|
+
value.forEach(id => validateUserId(id, "Parallel axis scale id"));
|
|
37
|
+
} else if (property === "title") {
|
|
38
|
+
validateNonEmptySemanticString(value, "Axis title");
|
|
39
|
+
} else {
|
|
40
|
+
const guideKind = parsed.id.startsWith("grid.") ? "Grid" : "Axis";
|
|
41
|
+
validateUserId(value, `${guideKind} ${property} id`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { validateCoordinateType } from "../../../grammar/coordinates.js";
|
|
2
|
+
import { validateDatasetSemanticValue } from "./dataset.js";
|
|
3
|
+
import { validateGuideSemanticValue } from "./guide.js";
|
|
4
|
+
import { validateLayerSemanticValue } from "./layer.js";
|
|
5
|
+
import { validateScaleSemanticValue } from "./scale.js";
|
|
6
|
+
import { validateNonEmptySemanticString } from "./shared.js";
|
|
7
|
+
|
|
8
|
+
const VALIDATORS = Object.freeze({
|
|
9
|
+
dataset: validateDatasetSemanticValue,
|
|
10
|
+
layer: validateLayerSemanticValue,
|
|
11
|
+
scale: validateScaleSemanticValue,
|
|
12
|
+
coordinate(_program, parsed, value) {
|
|
13
|
+
if (parsed.path[0] === "type") validateCoordinateType(value);
|
|
14
|
+
},
|
|
15
|
+
guide: validateGuideSemanticValue,
|
|
16
|
+
title(_program, parsed, value) {
|
|
17
|
+
validateNonEmptySemanticString(value, `Chart title ${parsed.path[0]}`);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export function validateSemanticValue(program, parsed, value) {
|
|
22
|
+
VALIDATORS[parsed.kind]?.(program, parsed, value);
|
|
23
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { validateUserId } from "../../../core/identifiers.js";
|
|
2
|
+
import {
|
|
3
|
+
COLOR_LAYOUTS,
|
|
4
|
+
MARK_TYPES,
|
|
5
|
+
STACK_MODES
|
|
6
|
+
} from "../../../core/vocabulary.js";
|
|
7
|
+
import { validateAggregate } from "../../../grammar/aggregate.js";
|
|
8
|
+
import {
|
|
9
|
+
normalizeHistogramBin,
|
|
10
|
+
validateHistogramBinBoundaries,
|
|
11
|
+
validateHistogramBinStep
|
|
12
|
+
} from "../../../grammar/histogram.js";
|
|
13
|
+
import {
|
|
14
|
+
validateParallelDimensions,
|
|
15
|
+
validateParallelKeyField,
|
|
16
|
+
validateParallelMissingPolicy
|
|
17
|
+
} from "../../../grammar/parallelCoordinates.js";
|
|
18
|
+
import { validatePathOrderDirection } from "../../../grammar/pathOrder.js";
|
|
19
|
+
import { validateSemanticFieldType } from "../../../grammar/scales/index.js";
|
|
20
|
+
import { findLayer } from "../../../selectors/layers.js";
|
|
21
|
+
import { validateNonEmptySemanticString } from "./shared.js";
|
|
22
|
+
|
|
23
|
+
function validateLayerSource(program, parsed, value) {
|
|
24
|
+
validateUserId(value, "Layer source id");
|
|
25
|
+
if (value === parsed.id) {
|
|
26
|
+
throw new Error("A layer cannot use itself as its source.");
|
|
27
|
+
}
|
|
28
|
+
const source = findLayer(program, value);
|
|
29
|
+
if (source === undefined) {
|
|
30
|
+
throw new Error(`Unknown source layer "${value}".`);
|
|
31
|
+
}
|
|
32
|
+
if (!["point", "bar", "rule", "rect"].includes(source.mark?.type)) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`Layer source "${value}" must be a point, bar, rule, or rect mark.`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function validateLayerSemanticValue(program, parsed, value) {
|
|
40
|
+
const property = parsed.path.join(".");
|
|
41
|
+
if (property === "mark.type" && !MARK_TYPES.includes(value)) {
|
|
42
|
+
throw new Error(`Unknown mark type "${value}".`);
|
|
43
|
+
}
|
|
44
|
+
if (property === "source") validateLayerSource(program, parsed, value);
|
|
45
|
+
if (property.endsWith(".title")) {
|
|
46
|
+
validateNonEmptySemanticString(value, "Encoding title");
|
|
47
|
+
}
|
|
48
|
+
if (property === "encoding.pathOrder.fieldType" && value !== "quantitative") {
|
|
49
|
+
throw new Error("Path order field type must be quantitative.");
|
|
50
|
+
}
|
|
51
|
+
if (property.endsWith(".fieldType")) validateSemanticFieldType(value);
|
|
52
|
+
if (property === "encoding.pathOrder.order") validatePathOrderDirection(value);
|
|
53
|
+
if (property === "encoding.parallel.dimensions") {
|
|
54
|
+
validateParallelDimensions(value, { normalized: true });
|
|
55
|
+
}
|
|
56
|
+
if (property === "encoding.parallel.key") validateParallelKeyField(value);
|
|
57
|
+
if (property === "encoding.parallel.missing") {
|
|
58
|
+
validateParallelMissingPolicy(value);
|
|
59
|
+
}
|
|
60
|
+
if (property.endsWith(".aggregate")) validateAggregate(value);
|
|
61
|
+
if (property.endsWith(".bin.maxBins")) normalizeHistogramBin({ maxBins: value });
|
|
62
|
+
if (property.endsWith(".bin.step")) validateHistogramBinStep(value);
|
|
63
|
+
if (property.endsWith(".bin.boundaries")) validateHistogramBinBoundaries(value);
|
|
64
|
+
if (
|
|
65
|
+
property.endsWith(".stack") &&
|
|
66
|
+
value !== null &&
|
|
67
|
+
!STACK_MODES.includes(value)
|
|
68
|
+
) {
|
|
69
|
+
throw new Error(`Unsupported stack "${value}".`);
|
|
70
|
+
}
|
|
71
|
+
if (property === "encoding.color.layout" && !COLOR_LAYOUTS.includes(value)) {
|
|
72
|
+
throw new Error(`Unsupported color layout "${value}".`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {
|
|
2
|
+
validateContinuousColorInterpolation,
|
|
3
|
+
validateScalePropertyForType,
|
|
4
|
+
validateSemanticScaleDomain,
|
|
5
|
+
validateSemanticScaleRange,
|
|
6
|
+
validateSemanticScaleType
|
|
7
|
+
} from "../../../grammar/scales/index.js";
|
|
8
|
+
import { findSemanticScale } from "../../../selectors/scales.js";
|
|
9
|
+
|
|
10
|
+
function validateOwnedProperty(existing, property) {
|
|
11
|
+
if (existing?.type !== undefined) {
|
|
12
|
+
validateScalePropertyForType(existing.type, property);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function validateScaleSemanticValue(program, parsed, value) {
|
|
17
|
+
const property = parsed.path.join(".");
|
|
18
|
+
const existing = findSemanticScale(program, parsed.id);
|
|
19
|
+
if (property === "type") {
|
|
20
|
+
validateSemanticScaleType(value);
|
|
21
|
+
for (const owned of [
|
|
22
|
+
"nice", "zero", "clamp", "base", "exponent", "constant",
|
|
23
|
+
"paddingInner", "paddingOuter", "padding", "align"
|
|
24
|
+
]) {
|
|
25
|
+
if (existing?.[owned] !== undefined) validateScalePropertyForType(value, owned);
|
|
26
|
+
}
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (property === "domain") return validateSemanticScaleDomain(value);
|
|
30
|
+
if (property === "range") return validateSemanticScaleRange(value);
|
|
31
|
+
if (["nice", "zero", "clamp", "reverse"].includes(property)) {
|
|
32
|
+
if (typeof value !== "boolean") {
|
|
33
|
+
throw new TypeError(`Scale ${property} must be a boolean.`);
|
|
34
|
+
}
|
|
35
|
+
if (property !== "reverse") validateOwnedProperty(existing, property);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (["base", "exponent", "constant"].includes(property)) {
|
|
39
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
40
|
+
throw new RangeError(`Scale ${property} must be a positive finite number.`);
|
|
41
|
+
}
|
|
42
|
+
if (property === "base" && value === 1) {
|
|
43
|
+
throw new RangeError("Scale base must not equal 1.");
|
|
44
|
+
}
|
|
45
|
+
validateOwnedProperty(existing, property);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (property === "interpolate") {
|
|
49
|
+
validateContinuousColorInterpolation(value);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (property === "paddingInner") {
|
|
53
|
+
if (!Number.isFinite(value) || value < 0 || value >= 1) {
|
|
54
|
+
throw new RangeError(
|
|
55
|
+
"Scale paddingInner must be from 0 (inclusive) to 1 (exclusive)."
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
validateOwnedProperty(existing, property);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (property === "paddingOuter" || property === "padding") {
|
|
62
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
63
|
+
throw new RangeError(`Scale ${property} must be a non-negative finite number.`);
|
|
64
|
+
}
|
|
65
|
+
validateOwnedProperty(existing, property);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (property === "align") {
|
|
69
|
+
if (!Number.isFinite(value) || value < 0 || value > 1) {
|
|
70
|
+
throw new RangeError("Scale align must be between 0 and 1.");
|
|
71
|
+
}
|
|
72
|
+
validateOwnedProperty(existing, property);
|
|
73
|
+
}
|
|
74
|
+
}
|