ggaction 0.0.4 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/README.md +35 -13
- package/package.json +18 -7
- package/src/actions/boxPlots/create.js +13 -7
- package/src/actions/boxPlots/edit.js +31 -35
- package/src/actions/boxPlots/materialize.js +3 -0
- package/src/actions/boxPlots/options.js +8 -1
- package/src/actions/boxPlots/resolve.js +10 -2
- package/src/actions/charts/bar.js +50 -0
- package/src/actions/charts/heatmap.js +205 -0
- package/src/actions/charts/histogram.js +58 -0
- package/src/actions/charts/index.js +15 -0
- package/src/actions/charts/line.js +69 -0
- package/src/actions/charts/parallel.js +76 -0
- package/src/actions/charts/scatter.js +56 -0
- package/src/actions/charts/shared.js +121 -0
- package/src/actions/coordinates/parallel.js +50 -0
- package/src/actions/data/bin2d.js +137 -0
- package/src/actions/data/density.js +54 -13
- package/src/actions/data/derived.js +22 -0
- package/src/actions/data/filter.js +5 -0
- package/src/actions/data/gradientProfile.js +65 -0
- package/src/actions/data/horizon.js +71 -0
- package/src/actions/data/index.js +29 -2
- package/src/actions/data/window.js +50 -0
- package/src/actions/encodings/color/index.js +17 -1
- package/src/actions/encodings/color/layout.js +8 -0
- package/src/actions/encodings/density/resolve.js +109 -0
- package/src/actions/encodings/density.js +193 -88
- package/src/actions/encodings/horizon/resolve.js +283 -0
- package/src/actions/encodings/horizon.js +364 -0
- package/src/actions/encodings/index.js +6 -0
- package/src/actions/encodings/parallel.js +115 -0
- package/src/actions/encodings/pathOrder.js +131 -0
- package/src/actions/encodings/position/apply.js +73 -4
- package/src/actions/encodings/position/index.js +15 -2
- package/src/actions/encodings/position/policies/arc.js +27 -4
- package/src/actions/encodings/position/policies/area.js +33 -2
- package/src/actions/encodings/position/policies/index.js +7 -2
- package/src/actions/encodings/position/policies/line.js +18 -2
- package/src/actions/encodings/position/resolve.js +24 -2
- package/src/actions/encodings/ranged.js +3 -1
- package/src/actions/encodings/ruleAppearance.js +88 -10
- package/src/actions/facets/actions.js +1 -2
- package/src/actions/facets/derive.js +33 -3
- package/src/actions/facets/replay.js +7 -36
- package/src/actions/gradientPlots/components.js +177 -0
- package/src/actions/gradientPlots/create.js +96 -0
- package/src/actions/gradientPlots/edit.js +153 -0
- package/src/actions/gradientPlots/index.js +24 -0
- package/src/actions/gradientPlots/materialize.js +287 -0
- package/src/actions/gradientPlots/options.js +120 -0
- package/src/actions/gradientPlots/paint.js +107 -0
- package/src/actions/gradientPlots/rebind.js +51 -0
- package/src/actions/gradientPlots/resolve.js +81 -0
- package/src/actions/guides/applicability.js +44 -17
- package/src/actions/guides/axes/axes.js +37 -3
- package/src/actions/guides/axes/index.js +2 -0
- package/src/actions/guides/axes/labels.js +11 -6
- package/src/actions/guides/axes/parallel/resolve.js +86 -0
- package/src/actions/guides/axes/parallel.js +228 -0
- package/src/actions/guides/axes/titles.js +11 -8
- package/src/actions/guides/guides.js +60 -5
- package/src/actions/guides/legends/categorical/actions.js +48 -11
- package/src/actions/guides/legends/edit.js +5 -0
- package/src/actions/guides/legends/index.js +2 -0
- package/src/actions/guides/legends/remove.js +4 -11
- package/src/actions/guides/legends/strokeWidth.js +170 -0
- package/src/actions/index.js +6 -0
- package/src/actions/marks/area/actions.js +60 -136
- package/src/actions/marks/area/materialize.js +163 -0
- package/src/actions/marks/index.js +5 -1
- package/src/actions/marks/line/actions.js +99 -80
- package/src/actions/marks/line/materialize.js +146 -0
- package/src/actions/marks/point/create.js +76 -0
- package/src/actions/marks/point/edit.js +85 -0
- package/src/actions/marks/point/index.js +11 -1
- package/src/actions/marks/point/jitter.js +72 -0
- package/src/actions/marks/point/{actions.js → materialize.js} +175 -170
- package/src/actions/marks/rect/actions.js +3 -0
- package/src/actions/marks/remove.js +13 -2
- package/src/actions/marks/rule/actions.js +25 -3
- package/src/actions/marks/text/actions.js +19 -4
- package/src/actions/marks/text/index.js +7 -1
- package/src/actions/marks/text/layout.js +321 -0
- package/src/actions/primitives/semanticValidation/dataset.js +22 -0
- package/src/actions/primitives/semanticValidation/guide.js +43 -0
- package/src/actions/primitives/semanticValidation/index.js +23 -0
- package/src/actions/primitives/semanticValidation/layer.js +74 -0
- package/src/actions/primitives/semanticValidation/scale.js +74 -0
- package/src/actions/primitives/semanticValidation/shared.js +5 -0
- package/src/actions/primitives/semanticValue.js +1 -204
- package/src/actions/regression/edit.js +19 -21
- package/src/actions/scales/consumers/common.js +18 -0
- package/src/actions/scales/consumers/families.js +14 -3
- package/src/actions/scales/definitions.js +30 -0
- package/src/actions/scales/edit.js +6 -204
- package/src/actions/scales/editPolicy.js +214 -0
- package/src/actions/scales/materialize.js +1 -1
- package/src/actions/scales/patch.js +26 -0
- package/src/actions/selection/actions.js +28 -3
- package/src/actions/violinPlots/create.js +198 -0
- package/src/actions/violinPlots/index.js +5 -0
- package/src/core/textMetrics.js +47 -0
- package/src/core/vocabulary.js +7 -2
- package/src/grammar/arcs.js +49 -12
- package/src/grammar/areaSeries.js +41 -5
- package/src/grammar/bin2d.js +333 -0
- package/src/grammar/categoricalDensity.js +192 -0
- package/src/grammar/coordinates.js +1 -1
- package/src/grammar/curveCommands.js +3 -1
- package/src/grammar/density.js +252 -26
- package/src/grammar/facets/dependencies.js +3 -11
- package/src/grammar/facets/index.js +3 -1
- package/src/grammar/gradientProfile.js +213 -0
- package/src/grammar/horizon.js +455 -0
- package/src/grammar/jitter.js +197 -0
- package/src/grammar/lineSeries.js +69 -0
- package/src/grammar/paint.js +88 -0
- package/src/grammar/parallelCoordinates.js +213 -0
- package/src/grammar/pathOrder.js +35 -0
- package/src/grammar/pointShapes.js +24 -0
- package/src/grammar/rules.js +1 -0
- package/src/grammar/scales/appearance.js +21 -0
- package/src/grammar/schemas/concreteGraphic.js +11 -1
- package/src/grammar/schemas/graphicBounds.js +19 -37
- package/src/grammar/schemas/semanticPath.js +17 -2
- package/src/grammar/transforms.js +100 -11
- package/src/grammar/window.js +339 -0
- package/src/layout/labels.js +270 -0
- package/src/layout/text.js +4 -1
- package/src/layout/title.js +10 -20
- package/src/materialization/dataProvenance.js +56 -2
- package/src/materialization/dependencies.js +9 -14
- package/src/materialization/guides/resources.js +74 -24
- package/src/materialization/horizon.js +29 -0
- package/src/materialization/layout.js +15 -0
- package/src/materialization/marks/capabilities.js +21 -1
- package/src/materialization/marks/index.js +3 -0
- package/src/materialization/marks/pathOrder.js +71 -0
- package/src/materialization/marks/policies.js +6 -2
- package/src/materialization/scaleGuideDependencies.js +8 -0
- package/src/materialization/scales/resolve.js +2 -0
- package/src/materialization/selection/items/path.js +27 -1
- package/src/materialization/selection/items/point.js +1 -5
- package/src/materialization/selection/items/rect.js +21 -0
- package/src/materialization/selection/styles.js +20 -9
- package/src/materialization/text.js +12 -4
- package/src/renderers/canvas/fill.js +33 -0
- package/src/renderers/canvas/index.js +7 -0
- package/src/renderers/canvas/path.js +7 -4
- package/src/renderers/canvas/rect.js +9 -3
- package/src/selectors/datasets.js +4 -0
- package/src/theme/defaults.js +4 -0
- package/types/extension.d.ts +6 -0
- package/types/index.d.ts +76 -0
- package/types/program.d.ts +632 -10
|
@@ -104,7 +104,17 @@ const createRuleMark = action(
|
|
|
104
104
|
length: 0,
|
|
105
105
|
...resolveMarkGraphicPlacement(created, { data, markType: "rule" })
|
|
106
106
|
})
|
|
107
|
-
._withMarkConfig(id,
|
|
107
|
+
._withMarkConfig(id, {
|
|
108
|
+
...DEFAULT_RULE_CONFIG,
|
|
109
|
+
...(inherited === undefined
|
|
110
|
+
? {}
|
|
111
|
+
: {
|
|
112
|
+
inheritedPosition: {
|
|
113
|
+
source: inherited.source,
|
|
114
|
+
channels: Object.keys(inherited.encoding)
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
});
|
|
108
118
|
return materializeInheritedMark(created, id);
|
|
109
119
|
}
|
|
110
120
|
);
|
|
@@ -143,7 +153,7 @@ const rematerializeRuleMark = action(
|
|
|
143
153
|
}
|
|
144
154
|
|
|
145
155
|
const scaleIds = [...new Set(
|
|
146
|
-
["x", "y", "x2", "y2", "strokeDash", "opacity"]
|
|
156
|
+
["x", "y", "x2", "y2", "strokeDash", "strokeWidth", "opacity"]
|
|
147
157
|
.map(channel => layer.encoding?.[channel]?.scale)
|
|
148
158
|
.filter(scale => scale !== undefined)
|
|
149
159
|
)];
|
|
@@ -153,6 +163,11 @@ const rematerializeRuleMark = action(
|
|
|
153
163
|
}
|
|
154
164
|
|
|
155
165
|
const derived = deriveRuleValues(dataset.values, layer);
|
|
166
|
+
if (derived.values.strokeWidth?.some(value => value < 0)) {
|
|
167
|
+
throw new RangeError(
|
|
168
|
+
`Rule strokeWidth field "${layer.encoding.strokeWidth.field}" cannot contain negative values.`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
156
171
|
const mapped = {};
|
|
157
172
|
for (const channel of ["x", "y", "x2", "y2"]) {
|
|
158
173
|
const encoding = layer.encoding?.[channel];
|
|
@@ -226,6 +241,7 @@ const rematerializeRuleMark = action(
|
|
|
226
241
|
const resolvedConfig = resolved.markConfigs[id] ?? DEFAULT_RULE_CONFIG;
|
|
227
242
|
const dashEncoding = layer.encoding?.strokeDash;
|
|
228
243
|
const opacityEncoding = layer.encoding?.opacity;
|
|
244
|
+
const strokeWidthEncoding = layer.encoding?.strokeWidth;
|
|
229
245
|
const strokeDash = dashEncoding?.scale === undefined
|
|
230
246
|
? Array.from(
|
|
231
247
|
{ length: derived.length },
|
|
@@ -244,6 +260,12 @@ const rematerializeRuleMark = action(
|
|
|
244
260
|
derived.values.opacity,
|
|
245
261
|
resolved.resolvedScales[opacityEncoding.scale]
|
|
246
262
|
);
|
|
263
|
+
const strokeWidth = strokeWidthEncoding?.scale === undefined
|
|
264
|
+
? resolvedConfig.strokeWidth
|
|
265
|
+
: mapContinuousScaleValues(
|
|
266
|
+
derived.values.strokeWidth,
|
|
267
|
+
resolved.resolvedScales[strokeWidthEncoding.scale]
|
|
268
|
+
);
|
|
247
269
|
|
|
248
270
|
return resolved
|
|
249
271
|
.editGraphics({ target: id, property: "length", value: derived.length })
|
|
@@ -255,7 +277,7 @@ const rematerializeRuleMark = action(
|
|
|
255
277
|
.editGraphics({
|
|
256
278
|
target: id,
|
|
257
279
|
property: "strokeWidth",
|
|
258
|
-
value:
|
|
280
|
+
value: strokeWidth
|
|
259
281
|
})
|
|
260
282
|
.editGraphics({ target: id, property: "strokeDash", value: strokeDash })
|
|
261
283
|
.editGraphics({ target: id, property: "opacity", value: opacity });
|
|
@@ -24,14 +24,19 @@ const STYLE_OPTIONS = Object.freeze([
|
|
|
24
24
|
]);
|
|
25
25
|
const CREATE_OPTIONS = Object.freeze(["id", "data", "text", ...STYLE_OPTIONS]);
|
|
26
26
|
const EDIT_OPTIONS = Object.freeze(["target", ...STYLE_OPTIONS]);
|
|
27
|
-
const REMATERIALIZE_OPTIONS = Object.freeze(["id"]);
|
|
27
|
+
const REMATERIALIZE_OPTIONS = Object.freeze(["id", "replayLayout"]);
|
|
28
28
|
const SOURCE_TYPES = new Set(["point", "bar", "rule", "rect"]);
|
|
29
29
|
|
|
30
|
+
function sourceMatchesData(program, layer, requestedData) {
|
|
31
|
+
if (requestedData === undefined || layer?.data === requestedData) return true;
|
|
32
|
+
return program.markConfigs[layer.id]?.gradientPlot?.source === requestedData;
|
|
33
|
+
}
|
|
34
|
+
|
|
30
35
|
function eligibleSource(program, layer, requestedData) {
|
|
31
36
|
if (
|
|
32
37
|
!SOURCE_TYPES.has(layer?.mark?.type) ||
|
|
33
38
|
layer.data === undefined ||
|
|
34
|
-
(
|
|
39
|
+
!sourceMatchesData(program, layer, requestedData)
|
|
35
40
|
) return false;
|
|
36
41
|
const encodings = resolveCompatibleEncodings(program, layer, "text");
|
|
37
42
|
return encodings.x !== undefined && encodings.y !== undefined;
|
|
@@ -133,6 +138,10 @@ const rematerializeTextMark = action(
|
|
|
133
138
|
function (args = {}) {
|
|
134
139
|
validateMarkOptions(args, REMATERIALIZE_OPTIONS, "rematerializeTextMark");
|
|
135
140
|
const id = validateUserId(args.id, "Text mark id");
|
|
141
|
+
const replayLayout = args.replayLayout ?? true;
|
|
142
|
+
if (typeof replayLayout !== "boolean") {
|
|
143
|
+
throw new TypeError("rematerializeTextMark replayLayout must be a boolean.");
|
|
144
|
+
}
|
|
136
145
|
const layer = findLayer(this, id);
|
|
137
146
|
const graphic = this.graphicSpec.objects[id];
|
|
138
147
|
if (layer?.mark?.type !== "text") throw new Error(`Unknown text mark "${id}".`);
|
|
@@ -140,11 +149,14 @@ const rematerializeTextMark = action(
|
|
|
140
149
|
throw new Error(`Text mark "${id}" requires text collection graphics.`);
|
|
141
150
|
}
|
|
142
151
|
if (!canMaterializeText(this, layer)) {
|
|
143
|
-
|
|
152
|
+
const next = graphic.items.length === 0
|
|
144
153
|
? this
|
|
145
154
|
: this.editGraphics({ target: id, property: "length", value: 0 });
|
|
155
|
+
return replayLayout && next.materializationConfigs.labelLayouts?.[id] !== undefined
|
|
156
|
+
? next.materializeLabelLayout({ id, rematerializeBase: false })
|
|
157
|
+
: next;
|
|
146
158
|
}
|
|
147
|
-
|
|
159
|
+
const next = this.editGraphics({
|
|
148
160
|
target: id,
|
|
149
161
|
property: "items",
|
|
150
162
|
value: resolveTextGraphicItems(
|
|
@@ -153,6 +165,9 @@ const rematerializeTextMark = action(
|
|
|
153
165
|
this.markConfigs[id] ?? DEFAULT_TEXT_MARK
|
|
154
166
|
)
|
|
155
167
|
});
|
|
168
|
+
return replayLayout && next.materializationConfigs.labelLayouts?.[id] !== undefined
|
|
169
|
+
? next.materializeLabelLayout({ id, rematerializeBase: false })
|
|
170
|
+
: next;
|
|
156
171
|
}
|
|
157
172
|
);
|
|
158
173
|
|
|
@@ -1 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
import { registerTextMarkActions as registerTextCoreActions } from "./actions.js";
|
|
2
|
+
import { registerTextLabelLayoutActions } from "./layout.js";
|
|
3
|
+
|
|
4
|
+
export function registerTextMarkActions(ProgramClass) {
|
|
5
|
+
registerTextCoreActions(ProgramClass);
|
|
6
|
+
registerTextLabelLayoutActions(ProgramClass);
|
|
7
|
+
}
|
|
@@ -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
|
+
}
|