ggaction 0.0.18 → 0.0.19
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 +50 -0
- package/README.md +6 -2
- package/knowledge/action-cards.json +1 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/package.json +1 -1
- package/src/actions/canvas/fitting.js +6 -2
- package/src/actions/charts/heatmap.js +4 -3
- package/src/actions/data/derived.js +11 -1
- package/src/actions/data/edit.js +2 -0
- package/src/actions/data/index.js +4 -0
- package/src/actions/data/regression.js +24 -16
- package/src/actions/data/regressionStatistics.js +73 -0
- package/src/actions/data/shared.js +5 -2
- package/src/actions/distributions/revision.js +24 -18
- package/src/actions/encodings/remove.js +11 -6
- package/src/actions/encodings/shared.js +26 -24
- package/src/actions/facets/actions.js +18 -7
- package/src/actions/facets/guides.js +2 -1
- package/src/actions/guides/axes/axes.js +30 -11
- package/src/actions/guides/axes/axis.js +58 -8
- package/src/actions/guides/axes/components.js +13 -13
- package/src/actions/guides/axes/edit.js +36 -24
- package/src/actions/guides/axes/labels.js +37 -27
- package/src/actions/guides/axes/lines.js +22 -20
- package/src/actions/guides/axes/remove.js +10 -6
- package/src/actions/guides/axes/tickGroups.js +8 -7
- package/src/actions/guides/axes/ticks.js +31 -22
- package/src/actions/guides/axes/titles.js +53 -60
- package/src/actions/guides/facadeAxes.js +33 -24
- package/src/actions/guides/facadeLegend.js +3 -3
- package/src/actions/guides/grids/grid.js +1 -1
- package/src/actions/guides/grids/resolve.js +48 -51
- package/src/actions/guides/legends/blocks.js +43 -19
- package/src/actions/guides/legends/categorical/actions.js +30 -7
- package/src/actions/guides/legends/categorical/components.js +3 -10
- package/src/actions/guides/legends/categorical/index.js +2 -0
- package/src/actions/guides/legends/categorical/layout.js +9 -13
- package/src/actions/guides/legends/categorical/options.js +8 -2
- package/src/actions/guides/legends/categorical/recipes.js +7 -3
- package/src/actions/guides/legends/categorical/resolve.js +9 -2
- package/src/actions/guides/legends/categorical/symbols.js +22 -8
- package/src/actions/guides/legends/colorSize.js +110 -0
- package/src/actions/guides/legends/combine.js +37 -0
- package/src/actions/guides/legends/continuous/colorSymbols.js +1 -1
- package/src/actions/guides/legends/continuous/common.js +1 -1
- package/src/actions/guides/legends/continuous/gradient.js +27 -13
- package/src/actions/guides/legends/continuous/opacity.js +16 -11
- package/src/actions/guides/legends/creation.js +2 -0
- package/src/actions/guides/legends/edit.js +1 -1
- package/src/actions/guides/legends/focused.js +6 -5
- package/src/actions/guides/legends/lane.js +1 -1
- package/src/actions/guides/legends/lifecycle.js +2 -1
- package/src/actions/guides/legends/remove.js +6 -3
- package/src/actions/guides/legends/size.js +16 -6
- package/src/actions/guides/legends/strokeWidth.js +2 -0
- package/src/actions/guides/legends/target.js +2 -79
- package/src/actions/guides/legends/transition.js +24 -5
- package/src/actions/guides/tickValues.js +8 -5
- package/src/actions/marks/bar/edit.js +23 -12
- package/src/actions/marks/bar/materialize.js +6 -1
- package/src/actions/marks/point/materialize.js +5 -1
- package/src/actions/marks/remove.js +10 -10
- package/src/actions/primitives/createGraphics.js +3 -2
- package/src/actions/primitives/editGraphics.js +15 -11
- package/src/actions/primitives/graphicProperties.js +1 -6
- package/src/actions/primitives/semanticAction.js +27 -1
- package/src/actions/primitives/semanticValidation/guide.js +11 -1
- package/src/actions/primitives/semanticValidation/layer.js +4 -0
- package/src/actions/regression/edit.js +15 -3
- package/src/actions/scales/channels.js +2 -2
- package/src/actions/scales/create.js +3 -3
- package/src/actions/scales/definitions.js +1 -1
- package/src/actions/scales/edit.js +1 -1
- package/src/actions/scales/editPolicy.js +3 -0
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/quantitativeColor.js +8 -3
- package/src/actions/theme/reconcile.js +50 -23
- package/src/core/compositionState.js +2 -2
- package/src/core/graphicProperties.js +1 -0
- package/src/core/programState.js +4 -0
- package/src/core/resourceReferences.js +2 -2
- package/src/core/validation.js +9 -0
- package/src/core/vocabulary.js +3 -0
- package/src/grammar/axisTextPlacement.js +1 -0
- package/src/grammar/colorSizeLegend.js +39 -0
- package/src/grammar/computed.js +23 -1
- package/src/grammar/datasetSchema.js +10 -2
- package/src/grammar/displayLabels.js +8 -0
- package/src/grammar/facets/guides.js +54 -31
- package/src/grammar/facets/legendIntent.js +65 -0
- package/src/grammar/guideDash.js +1 -0
- package/src/grammar/legendBlocks.js +1 -0
- package/src/grammar/legendBorder.js +9 -0
- package/src/grammar/legendClip.js +19 -0
- package/src/grammar/legendCombinations.js +45 -0
- package/src/grammar/legendGradient.js +9 -0
- package/src/grammar/legendGroups.js +1 -1
- package/src/grammar/legendOpacity.js +6 -0
- package/src/grammar/pointShapes.js +16 -2
- package/src/grammar/regression/derive.js +9 -1
- package/src/grammar/regression/models.js +60 -0
- package/src/grammar/regression/parameters.js +9 -1
- package/src/grammar/regression/statistics.js +64 -0
- package/src/grammar/scales/color.js +1 -1
- package/src/grammar/scales/colorConsumers.js +1 -1
- package/src/grammar/scales/definition.js +1 -1
- package/src/grammar/scales/types.js +1 -1
- package/src/grammar/schemas/concreteGraphic.js +3 -0
- package/src/grammar/schemas/graphic.js +3 -0
- package/src/grammar/schemas/graphicBounds.js +31 -7
- package/src/grammar/schemas/semanticPath.js +17 -2
- package/src/grammar/summary.js +1 -1
- package/src/grammar/timeUnit.js +6 -4
- package/src/grammar/transformTopology.js +1 -0
- package/src/grammar/transforms.js +11 -1
- package/src/inspectionDescriptors.js +1 -1
- package/src/layout/guideCollisions.js +9 -1
- package/src/layout/legendGroup.js +1 -1
- package/src/materialization/compositionSnapshot.js +2 -7
- package/src/materialization/coordinateBounds.js +17 -4
- package/src/materialization/facetGuides/edit.js +30 -0
- package/src/materialization/facetGuides/layout.js +1 -1
- package/src/materialization/facetGuides/owner.js +125 -0
- package/src/materialization/facetGuides/ownerLayout.js +71 -0
- package/src/materialization/facetGuides/placement.js +52 -6
- package/src/materialization/facetGuides/preparation.js +7 -0
- package/src/materialization/facets.js +1 -1
- package/src/materialization/graphicHierarchy.js +5 -1
- package/src/materialization/graphicTranslation.js +73 -0
- package/src/materialization/guides/layout.js +1 -1
- package/src/materialization/guides/resources.js +1 -1
- package/src/materialization/legendBorder.js +28 -0
- package/src/materialization/legendClip.js +76 -0
- package/src/materialization/legendGroups.js +1 -1
- package/src/materialization/legends.js +1 -1
- package/src/materialization/scaleGuideDependencies.js +13 -14
- package/src/materialization/scales/map.js +3 -0
- package/src/materialization/scales/resolve.js +2 -0
- package/src/materialization/typography.js +10 -2
- package/src/persistence/validation.js +174 -1
- package/src/persistence.js +1 -1
- package/src/renderers/canvas/index.js +20 -10
- package/src/renderers/canvas/isolated.js +166 -0
- package/src/renderers/canvas/limits.js +3 -0
- package/src/renderers/canvas/rect.js +1 -1
- package/src/renderers/pdf.js +2 -2
- package/src/renderers/png.js +2 -2
- package/src/renderers/svg.js +10 -3
- package/src/selectors/axes.js +1 -0
- package/src/selectors/graphicCapacity.js +38 -0
- package/src/version.js +1 -1
- package/types/basic.d.ts +2 -0
- package/types/index.d.ts +5 -0
- package/types/program.d.ts +206 -61
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { selectCartesianAxis, axisTargetArgs, assertCartesianAxisSideAvailable } from "../../../selectors/axes.js";
|
|
2
|
+
import { validateAxisPosition } from "./policy.js";
|
|
1
3
|
import { withGuideLayoutValidation } from "../../../materialization/guides/layout.js";
|
|
2
4
|
import { action } from "../../../core/action.js";
|
|
3
5
|
import { isPlainObject } from "../../../core/immutable.js";
|
|
4
|
-
import { validateKeys, validateOptionObject } from "../../../core/validation.js";
|
|
6
|
+
import { validateKeys, validateOptionObject, validateNonNegativeFinite } from "../../../core/validation.js";
|
|
5
7
|
|
|
6
8
|
import {
|
|
7
9
|
AXIS_COMPONENTS,
|
|
@@ -12,21 +14,21 @@ import {
|
|
|
12
14
|
} from "./components.js";
|
|
13
15
|
|
|
14
16
|
const OPTIONS = Object.freeze([
|
|
15
|
-
"position", "line", "ticks", "labels", "ticksAndLabels", "title"
|
|
17
|
+
"target", "position", "line", "ticks", "labels", "ticksAndLabels", "title", "offset"
|
|
16
18
|
]);
|
|
17
19
|
const LINE_OPTIONS = Object.freeze(["color", "lineWidth"]);
|
|
18
20
|
const TICK_OPTIONS = Object.freeze([
|
|
19
|
-
"count", "values", "length", "color", "lineWidth", "alongOffset"
|
|
21
|
+
"count", "values", "length", "color", "lineWidth", "alongOffset", "strokeDash", "strokeDashMap"
|
|
20
22
|
]);
|
|
21
23
|
const LABEL_OPTIONS = Object.freeze([
|
|
22
24
|
"count", "values", "offset", "format", "color", "fontSize",
|
|
23
25
|
"fontFamily", "fontWeight", "rotation", "maxWidth", "wrap",
|
|
24
|
-
"lineHeight", "overlap", "alongOffset", "labelMap", "flush", "colorMap"
|
|
26
|
+
"lineHeight", "overlap", "alongOffset", "labelMap", "flush", "colorMap", "align"
|
|
25
27
|
]);
|
|
26
28
|
const GROUP_OPTIONS = Object.freeze(["count", "values", "ticks", "labels"]);
|
|
27
29
|
const TITLE_OPTIONS = Object.freeze([
|
|
28
30
|
"text", "at", "offset", "rotation", "color", "fontSize",
|
|
29
|
-
"fontFamily", "fontWeight"
|
|
31
|
+
"fontFamily", "fontWeight", "alongOffset", "align", "baseline"
|
|
30
32
|
]);
|
|
31
33
|
|
|
32
34
|
function validateNested(args, key, options, operation, { allowFalse = false } = {}) {
|
|
@@ -40,6 +42,7 @@ function validateNested(args, key, options, operation, { allowFalse = false } =
|
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
function validateArgs(args, operation) {
|
|
45
|
+
if (Object.hasOwn(args, "offset")) validateNonNegativeFinite(args.offset, "Axis frame offset");
|
|
43
46
|
validateOptionObject(args, OPTIONS, operation, {
|
|
44
47
|
allowEmpty: false,
|
|
45
48
|
emptyMessage: `${operation} requires at least one axis change.`,
|
|
@@ -59,12 +62,12 @@ function validateArgs(args, operation) {
|
|
|
59
62
|
);
|
|
60
63
|
}
|
|
61
64
|
if (args.ticksAndLabels !== false && args.ticksAndLabels?.ticks !== undefined) {
|
|
62
|
-
validateNested(args.ticksAndLabels, "ticks", ["length", "color", "lineWidth", "alongOffset"], `${operation}.ticksAndLabels`);
|
|
65
|
+
validateNested(args.ticksAndLabels, "ticks", ["length", "color", "lineWidth", "alongOffset", "strokeDash", "strokeDashMap"], `${operation}.ticksAndLabels`);
|
|
63
66
|
}
|
|
64
67
|
if (args.ticksAndLabels !== false && args.ticksAndLabels?.labels !== undefined) {
|
|
65
68
|
validateNested(args.ticksAndLabels, "labels", [
|
|
66
69
|
"offset", "format", "color", "fontSize", "fontFamily", "fontWeight",
|
|
67
|
-
"rotation", "maxWidth", "wrap", "lineHeight", "overlap", "alongOffset", "labelMap", "flush", "colorMap"
|
|
70
|
+
"rotation", "maxWidth", "wrap", "lineHeight", "overlap", "alongOffset", "labelMap", "flush", "colorMap", "align"
|
|
68
71
|
], `${operation}.ticksAndLabels`);
|
|
69
72
|
}
|
|
70
73
|
}
|
|
@@ -82,27 +85,26 @@ function names(channel) {
|
|
|
82
85
|
};
|
|
83
86
|
}
|
|
84
87
|
|
|
85
|
-
function buildPlan(program, channel, args, operation) {
|
|
86
|
-
const shared =
|
|
87
|
-
? { position: args.position }
|
|
88
|
-
: {};
|
|
88
|
+
function buildPlan(program, channel, args, operation, axisId = channel) {
|
|
89
|
+
const shared = { ...axisTargetArgs(axisId, channel),
|
|
90
|
+
...(Object.hasOwn(args, "position") ? { position: args.position } : {}) };
|
|
89
91
|
const plan = [];
|
|
90
92
|
const addLeaf = (component, edit) => {
|
|
91
93
|
const value = args[component];
|
|
92
94
|
if (value === false) {
|
|
93
|
-
assertRemovable(program, channel, component, operation.operation);
|
|
95
|
+
assertRemovable(program, channel, component, operation.operation, axisId);
|
|
94
96
|
plan.push({ kind: "remove", component });
|
|
95
97
|
} else if (value !== undefined) {
|
|
96
98
|
plan.push({ kind: "edit", operation: edit, args: { ...shared, ...value } });
|
|
97
|
-
} else if (args.position !== undefined && hasComponent(program, channel, component)) {
|
|
99
|
+
} else if ((args.position !== undefined || Object.hasOwn(args, "offset")) && hasComponent(program, channel, component, axisId)) {
|
|
98
100
|
plan.push({ kind: "edit", operation: edit, args: shared });
|
|
99
101
|
}
|
|
100
102
|
};
|
|
101
103
|
|
|
102
104
|
addLeaf("line", operation.line);
|
|
103
105
|
if (args.ticksAndLabels === false) {
|
|
104
|
-
assertRemovable(program, channel, "ticks", operation.operation);
|
|
105
|
-
assertRemovable(program, channel, "labels", operation.operation);
|
|
106
|
+
assertRemovable(program, channel, "ticks", operation.operation, axisId);
|
|
107
|
+
assertRemovable(program, channel, "labels", operation.operation, axisId);
|
|
106
108
|
plan.push(
|
|
107
109
|
{ kind: "remove", component: "ticks" },
|
|
108
110
|
{ kind: "remove", component: "labels" }
|
|
@@ -125,22 +127,22 @@ function buildPlan(program, channel, args, operation) {
|
|
|
125
127
|
return plan;
|
|
126
128
|
}
|
|
127
129
|
|
|
128
|
-
function applyPlan(program, channel, plan, operation) {
|
|
130
|
+
function applyPlan(program, channel, plan, operation, axisId = channel) {
|
|
129
131
|
let next = program;
|
|
130
132
|
for (const step of plan) {
|
|
131
133
|
next = step.kind === "remove"
|
|
132
|
-
? removeComponent(next, channel, step.component)
|
|
134
|
+
? removeComponent(next, channel, step.component, axisId)
|
|
133
135
|
: next[step.operation](step.args);
|
|
134
136
|
}
|
|
135
137
|
|
|
136
138
|
const hasRetainedGraphic = AXIS_COMPONENTS.some(
|
|
137
|
-
component => next.graphicSpec.objects[graphicId(channel, component)] !== undefined
|
|
139
|
+
component => next.graphicSpec.objects[graphicId(channel, component, axisId)] !== undefined
|
|
138
140
|
);
|
|
139
141
|
if (!hasRetainedGraphic && (
|
|
140
|
-
next.semanticSpec.guides.axis?.[
|
|
141
|
-
next.guideConfigs.axis?.[
|
|
142
|
+
next.semanticSpec.guides.axis?.[axisId] !== undefined ||
|
|
143
|
+
next.guideConfigs.axis?.[axisId] !== undefined
|
|
142
144
|
)) {
|
|
143
|
-
next = next[operation.remove]();
|
|
145
|
+
next = next[operation.remove](axisId === channel ? undefined : axisTargetArgs(axisId, channel));
|
|
144
146
|
}
|
|
145
147
|
return next;
|
|
146
148
|
}
|
|
@@ -154,12 +156,22 @@ function makeEditAxis(channel) {
|
|
|
154
156
|
},
|
|
155
157
|
withGuideLayoutValidation(function (args = {}) {
|
|
156
158
|
validateArgs(args, operation.operation);
|
|
157
|
-
const
|
|
159
|
+
const selected = selectCartesianAxis(this, channel, args, { whole: true });
|
|
160
|
+
const axisId = selected.id; args = selected.options;
|
|
161
|
+
let source = this;
|
|
162
|
+
if (selected.owner?.named && args.position !== undefined) {
|
|
163
|
+
const position = validateAxisPosition(channel, args.position);
|
|
164
|
+
assertCartesianAxisSideAvailable(this, { id: axisId, channel,
|
|
165
|
+
coordinate: selected.owner.coordinate, position });
|
|
166
|
+
source = source.editSemantic({ property: `guide.axis.${axisId}.position`, value: position });
|
|
167
|
+
}
|
|
168
|
+
if (Object.hasOwn(args, "offset")) source = source._withGuideConfig(axisId, "frame", { offset: args.offset });
|
|
169
|
+
const plan = buildPlan(source, channel, args, operation, axisId);
|
|
158
170
|
|
|
159
171
|
// Run the complete proposal against an immutable speculative branch so a
|
|
160
172
|
// later leaf failure cannot begin the returned action trace or state.
|
|
161
|
-
applyPlan(
|
|
162
|
-
return applyPlan(
|
|
173
|
+
applyPlan(source, channel, plan, operation, axisId);
|
|
174
|
+
return applyPlan(source, channel, plan, operation, axisId);
|
|
163
175
|
})
|
|
164
176
|
);
|
|
165
177
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { selectCartesianAxis, axisTargetArgs } from "../../../selectors/axes.js";
|
|
2
|
+
import { axisGraphicIds } from "../../../materialization/guides/resources.js";
|
|
1
3
|
import { axisThemeTypography } from "../../theme/state.js";
|
|
4
|
+
import { validateAxisTextPlacement } from "../../../grammar/axisTextPlacement.js";
|
|
2
5
|
import { canvasOverflowError } from "../../../layout/canvas.js";
|
|
3
6
|
import { withGuideLayoutValidation } from "../../../materialization/guides/layout.js";
|
|
4
7
|
import { action } from "../../../core/action.js";
|
|
@@ -11,7 +14,7 @@ import {
|
|
|
11
14
|
validateNonNegativeFinite,
|
|
12
15
|
validatePositiveFinite
|
|
13
16
|
} from "../../../core/validation.js";
|
|
14
|
-
import {
|
|
17
|
+
import { resolveCartesianAxisFrameBounds } from "../../../materialization/coordinateBounds.js";
|
|
15
18
|
import {
|
|
16
19
|
isTransformedScaleType,
|
|
17
20
|
mapContinuousScaleValues,
|
|
@@ -48,9 +51,9 @@ import {
|
|
|
48
51
|
} from "../../../grammar/displayLabels.js";
|
|
49
52
|
|
|
50
53
|
const OPTIONS = [
|
|
51
|
-
"scale", "position", "count", "values", "offset", "format", "color",
|
|
54
|
+
"target", "scale", "position", "count", "values", "offset", "format", "color",
|
|
52
55
|
"fontSize", "fontFamily", "fontWeight", "rotation", "maxWidth", "wrap",
|
|
53
|
-
"lineHeight", "overlap", "labelMap", "alongOffset", "flush", "colorMap"
|
|
56
|
+
"lineHeight", "overlap", "labelMap", "alongOffset", "flush", "colorMap", "align"
|
|
54
57
|
];
|
|
55
58
|
|
|
56
59
|
const DEFAULTS = {
|
|
@@ -85,6 +88,7 @@ function normalizeLabelLayout(config, args, operation) {
|
|
|
85
88
|
export { validateTextStyle as validateAxisTextStyle };
|
|
86
89
|
|
|
87
90
|
function validateOptions(args, operation, create) {
|
|
91
|
+
validateAxisTextPlacement(args);
|
|
88
92
|
validateOptionObject(
|
|
89
93
|
args,
|
|
90
94
|
create ? OPTIONS : OPTIONS.filter(key => key !== "scale"),
|
|
@@ -98,6 +102,7 @@ function validateOptions(args, operation, create) {
|
|
|
98
102
|
}
|
|
99
103
|
|
|
100
104
|
function validateConfig(channel, config) {
|
|
105
|
+
validateAxisTextPlacement(config);
|
|
101
106
|
validateAxisPosition(channel, config.position);
|
|
102
107
|
if (config.mode === "count") {
|
|
103
108
|
if (!Number.isInteger(config.count) || config.count <= 0) throw new RangeError("Label count must be a positive integer.");
|
|
@@ -224,13 +229,9 @@ function unionLabelBounds(records) {
|
|
|
224
229
|
return [...byGroup.values()];
|
|
225
230
|
}
|
|
226
231
|
|
|
227
|
-
function resolve(program, channel, config) {
|
|
232
|
+
function resolve(program, channel, config, axisId = channel) {
|
|
228
233
|
const scale = program.resolvedScales[config.scale];
|
|
229
|
-
const bounds =
|
|
230
|
-
coordinate: program.semanticSpec.guides.axis?.[channel]?.coordinate,
|
|
231
|
-
scale: config.scale,
|
|
232
|
-
channel
|
|
233
|
-
});
|
|
234
|
+
const bounds = resolveCartesianAxisFrameBounds(program, channel, config.position, config.scale, axisId);
|
|
234
235
|
const discrete = ["ordinal", "band", "point"].includes(scale?.type);
|
|
235
236
|
validateAxisLabelMapScale(config, scale);
|
|
236
237
|
if ((
|
|
@@ -292,7 +293,7 @@ function resolve(program, channel, config) {
|
|
|
292
293
|
resolved.textBaseline = "middle";
|
|
293
294
|
}
|
|
294
295
|
const canvas = findCanvasGraphic(program)?.properties;
|
|
295
|
-
|
|
296
|
+
const measuredBounds = () => resolved.text.map((value, index) => resolveTextBounds({
|
|
296
297
|
x: Array.isArray(resolved.x) ? resolved.x[index] : resolved.x,
|
|
297
298
|
y: Array.isArray(resolved.y) ? resolved.y[index] : resolved.y,
|
|
298
299
|
text: value,
|
|
@@ -303,6 +304,7 @@ function resolve(program, channel, config) {
|
|
|
303
304
|
textBaseline: resolved.textBaseline,
|
|
304
305
|
rotation: config.rotation
|
|
305
306
|
}, program.materializationConfigs.textMetrics));
|
|
307
|
+
let labelBounds = measuredBounds();
|
|
306
308
|
if (program.materializationConfigs.canvas?.plot !== undefined || config.rotation !== 0) {
|
|
307
309
|
const horizontal = channel === "x";
|
|
308
310
|
const near = ["bottom", "right"].includes(config.position);
|
|
@@ -343,6 +345,10 @@ function resolve(program, channel, config) {
|
|
|
343
345
|
return { ...item, [lowKey]: item[lowKey] + delta, [highKey]: item[highKey] + delta };
|
|
344
346
|
});
|
|
345
347
|
}
|
|
348
|
+
if (config.align !== undefined) {
|
|
349
|
+
resolved.textAlign = config.align;
|
|
350
|
+
labelBounds = measuredBounds();
|
|
351
|
+
}
|
|
346
352
|
const rotatedOverlap = config.rotation !== 0;
|
|
347
353
|
const collisionBounds = rotatedOverlap ? resolved.text.map((text, index) => {
|
|
348
354
|
const x = Array.isArray(resolved.x) ? resolved.x[index] : resolved.x;
|
|
@@ -369,11 +375,11 @@ function resolve(program, channel, config) {
|
|
|
369
375
|
: index > 0 && textBoundsIntersect(orderedBounds[index - 1], item))) {
|
|
370
376
|
throw new Error(`The ${channel}-axis labels overlap each other.`);
|
|
371
377
|
}
|
|
372
|
-
const title = program.graphicSpec.objects[
|
|
373
|
-
? resolveConcreteGraphicBounds(program.graphicSpec,
|
|
378
|
+
const title = program.graphicSpec.objects[axisGraphicIds(channel, axisId)[3]]
|
|
379
|
+
? resolveConcreteGraphicBounds(program.graphicSpec, axisGraphicIds(channel, axisId)[3], program.materializationConfigs.textMetrics)
|
|
374
380
|
: undefined;
|
|
375
381
|
if (title &&
|
|
376
|
-
program.guideConfigs.axis?.[
|
|
382
|
+
program.guideConfigs.axis?.[axisId]?.title?.inferredOffset !== true &&
|
|
377
383
|
labelBounds.some(item => textBoundsIntersect(item, title))) {
|
|
378
384
|
throw new Error(`The ${channel}-axis labels overlap the axis title.`);
|
|
379
385
|
}
|
|
@@ -384,12 +390,14 @@ function makeEdit(channel) {
|
|
|
384
390
|
const op = channel === "x" ? "editXAxisLabels" : "editYAxisLabels";
|
|
385
391
|
return action({ op, description: `Edit concrete ${channel}-axis labels.` }, withGuideLayoutValidation(function (args = {}) {
|
|
386
392
|
validateOptions(args, op, false);
|
|
387
|
-
const
|
|
393
|
+
const selected = selectCartesianAxis(this, channel, args, { create: false });
|
|
394
|
+
const axisId = selected.id; args = selected.options;
|
|
395
|
+
const id = axisGraphicIds(channel, axisId)[2];
|
|
388
396
|
if (this.graphicSpec.objects[id]?.type !== "text") throw new Error(`${op} requires existing axis labels.`);
|
|
389
|
-
const previous = this.guideConfigs.axis?.[
|
|
397
|
+
const previous = this.guideConfigs.axis?.[axisId]?.labels;
|
|
390
398
|
if (!previous) throw new Error(`${op} requires label configuration.`);
|
|
391
399
|
const explicitMode = Object.hasOwn(args, "values") || Object.hasOwn(args, "count");
|
|
392
|
-
const ticks = this.guideConfigs.axis?.[
|
|
400
|
+
const ticks = this.guideConfigs.axis?.[axisId]?.ticks;
|
|
393
401
|
const inferredValues = !explicitMode && previous.inferredValues === true &&
|
|
394
402
|
ticks?.mode === "values"
|
|
395
403
|
? ticks.values
|
|
@@ -407,9 +415,9 @@ function makeEdit(channel) {
|
|
|
407
415
|
config = normalizeAxisLabelMaps(this, config, args, op);
|
|
408
416
|
if (mode === "values") delete config.count; else delete config.values;
|
|
409
417
|
validateConfig(channel, config);
|
|
410
|
-
assertTickCompatibility(this.guideConfigs.axis?.[
|
|
411
|
-
const resolved = resolve(this, channel, config);
|
|
412
|
-
let next = this._withGuideConfig(
|
|
418
|
+
assertTickCompatibility(this.guideConfigs.axis?.[axisId]?.ticks, config, op);
|
|
419
|
+
const resolved = resolve(this, channel, config, axisId);
|
|
420
|
+
let next = this._withGuideConfig(axisId, "labels", config);
|
|
413
421
|
const properties = {
|
|
414
422
|
length: resolved.text.length,
|
|
415
423
|
x: resolved.x,
|
|
@@ -430,10 +438,10 @@ function makeEdit(channel) {
|
|
|
430
438
|
for (const [property, value] of Object.entries(properties)) {
|
|
431
439
|
next = next.editGraphics({ target: id, property, value });
|
|
432
440
|
}
|
|
433
|
-
const titleConfig = next.guideConfigs.axis?.[
|
|
441
|
+
const titleConfig = next.guideConfigs.axis?.[axisId]?.title;
|
|
434
442
|
if (titleConfig?.inferredOffset === true) {
|
|
435
443
|
const editTitle = channel === "x" ? "editXAxisTitle" : "editYAxisTitle";
|
|
436
|
-
next = next[editTitle]();
|
|
444
|
+
next = next[editTitle](axisId === channel ? undefined : axisTargetArgs(axisId, channel));
|
|
437
445
|
}
|
|
438
446
|
return next;
|
|
439
447
|
}));
|
|
@@ -447,12 +455,14 @@ function makeCreate(channel) {
|
|
|
447
455
|
const edit = channel === "x" ? "editXAxisLabels" : "editYAxisLabels";
|
|
448
456
|
return action({ op, description: `Create concrete ${channel}-axis labels.` }, withGuideLayoutValidation(function (args = {}) {
|
|
449
457
|
validateOptions(args, op, true);
|
|
458
|
+
const selected = selectCartesianAxis(this, channel, args, { create: true });
|
|
459
|
+
const axisId = selected.id; args = selected.options;
|
|
450
460
|
const scale = validateUserId(args.scale ?? channel, "Scale id");
|
|
451
|
-
const guideScale = this.semanticSpec.guides.axis?.[
|
|
461
|
+
const guideScale = this.semanticSpec.guides.axis?.[axisId]?.scale;
|
|
452
462
|
if (guideScale && guideScale !== scale) throw new Error(`${op} conflicts with the existing axis scale.`);
|
|
453
|
-
const id =
|
|
463
|
+
const id = axisGraphicIds(channel, axisId)[2];
|
|
454
464
|
if (this.graphicSpec.objects[id]) throw new Error(`${op} requires missing axis labels.`);
|
|
455
|
-
const ticks = this.guideConfigs.axis?.[
|
|
465
|
+
const ticks = this.guideConfigs.axis?.[axisId]?.ticks;
|
|
456
466
|
const hasValues = Object.hasOwn(args, "values");
|
|
457
467
|
const hasCount = Object.hasOwn(args, "count");
|
|
458
468
|
const resolvedScale = this.resolvedScales[scale];
|
|
@@ -488,15 +498,15 @@ function makeCreate(channel) {
|
|
|
488
498
|
}
|
|
489
499
|
validateConfig(channel, config);
|
|
490
500
|
assertTickCompatibility(ticks, config, op);
|
|
491
|
-
resolve(this, channel, config);
|
|
492
|
-
return this.editSemantic({ property: `guide.axis.${
|
|
501
|
+
resolve(this, channel, config, axisId);
|
|
502
|
+
return this.editSemantic({ property: `guide.axis.${axisId}.scale`, value: scale })
|
|
493
503
|
.createGraphics({
|
|
494
504
|
id,
|
|
495
505
|
type: "text",
|
|
496
506
|
length: 0,
|
|
497
507
|
...resolvePlotGraphicPlacement(this)
|
|
498
508
|
})
|
|
499
|
-
._withGuideConfig(
|
|
509
|
+
._withGuideConfig(axisId, "labels", config)[edit](axisId === channel ? undefined : axisTargetArgs(axisId, channel));
|
|
500
510
|
}));
|
|
501
511
|
}
|
|
502
512
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { selectCartesianAxis, axisTargetArgs } from "../../../selectors/axes.js";
|
|
2
|
+
import { axisGraphicIds } from "../../../materialization/guides/resources.js";
|
|
1
3
|
import { requireSemanticScale } from "../../../selectors/scales.js";
|
|
2
4
|
import { editGraphicProperties } from "../../primitives/graphicProperties.js";
|
|
3
5
|
import { isSourceOwnedText } from "../../../grammar/text.js";
|
|
@@ -9,7 +11,7 @@ import {
|
|
|
9
11
|
validateNonEmptyString,
|
|
10
12
|
validateNonNegativeFinite
|
|
11
13
|
} from "../../../core/validation.js";
|
|
12
|
-
import { axisScaleBindings,
|
|
14
|
+
import { axisScaleBindings, resolveCartesianAxisFrameBounds } from "../../../materialization/coordinateBounds.js";
|
|
13
15
|
import { DEFAULT_COLORS } from "../../../theme/defaults.js";
|
|
14
16
|
import {
|
|
15
17
|
defaultAxisPosition,
|
|
@@ -20,8 +22,8 @@ import { resolvePlotGraphicPlacement } from
|
|
|
20
22
|
"../../../materialization/graphicHierarchy.js";
|
|
21
23
|
|
|
22
24
|
const DEFAULT_STYLE = Object.freeze({ color: DEFAULT_COLORS.text, lineWidth: 1 });
|
|
23
|
-
const CREATE_OPTIONS = Object.freeze(["scale", "position", "color", "lineWidth"]);
|
|
24
|
-
const EDIT_OPTIONS = Object.freeze(["position", "color", "lineWidth"]);
|
|
25
|
+
const CREATE_OPTIONS = Object.freeze(["target", "scale", "position", "color", "lineWidth"]);
|
|
26
|
+
const EDIT_OPTIONS = Object.freeze(["target", "position", "color", "lineWidth"]);
|
|
25
27
|
|
|
26
28
|
function validateStyle({ color, lineWidth }) {
|
|
27
29
|
validateNonEmptyString(color, "Axis line color");
|
|
@@ -42,7 +44,7 @@ function resolveLineRange(resolvedScale, bounds, channel, explicitRange) {
|
|
|
42
44
|
: plotRange.reverse();
|
|
43
45
|
}
|
|
44
46
|
|
|
45
|
-
function resolveGeometry(program, channel, scaleId, position) {
|
|
47
|
+
function resolveGeometry(program, channel, scaleId, position, axisId = channel) {
|
|
46
48
|
const hasConsumer = program.semanticSpec.layers.some(
|
|
47
49
|
layer => !isSourceOwnedText(layer) && layer.encoding?.[channel]?.scale === scaleId
|
|
48
50
|
);
|
|
@@ -55,11 +57,7 @@ function resolveGeometry(program, channel, scaleId, position) {
|
|
|
55
57
|
|
|
56
58
|
const resolvedScale = program.resolvedScales[scaleId];
|
|
57
59
|
const range = resolvedScale?.range;
|
|
58
|
-
const bounds =
|
|
59
|
-
coordinate: program.semanticSpec.guides.axis?.[channel]?.coordinate,
|
|
60
|
-
scale: scaleId,
|
|
61
|
-
channel
|
|
62
|
-
});
|
|
60
|
+
const bounds = resolveCartesianAxisFrameBounds(program, channel, position, scaleId, axisId);
|
|
63
61
|
|
|
64
62
|
if (!Array.isArray(range) || range.length !== 2 || !range.every(Number.isFinite)) {
|
|
65
63
|
throw new Error(`Axis line requires a resolved numeric scale "${scaleId}".`);
|
|
@@ -80,10 +78,10 @@ function resolveGeometry(program, channel, scaleId, position) {
|
|
|
80
78
|
);
|
|
81
79
|
}
|
|
82
80
|
|
|
83
|
-
function axisIds(channel) {
|
|
81
|
+
function axisIds(channel, axisId = channel) {
|
|
84
82
|
return {
|
|
85
|
-
graphic:
|
|
86
|
-
guidePath: `guide.axis.${
|
|
83
|
+
graphic: axisGraphicIds(channel, axisId)[0],
|
|
84
|
+
guidePath: `guide.axis.${axisId}.scale`
|
|
87
85
|
};
|
|
88
86
|
}
|
|
89
87
|
|
|
@@ -94,24 +92,26 @@ function createEditAxisLine(channel) {
|
|
|
94
92
|
{ op: operation, description: `Edit the concrete ${channel}-axis line.` },
|
|
95
93
|
withGuideLayoutValidation(function (args = {}) {
|
|
96
94
|
validateKeys(args, EDIT_OPTIONS, operation);
|
|
97
|
-
const
|
|
95
|
+
const selected = selectCartesianAxis(this, channel, args, { create: false });
|
|
96
|
+
const axisId = selected.id; args = selected.options;
|
|
97
|
+
const { graphic } = axisIds(channel, axisId);
|
|
98
98
|
const line = this.graphicSpec.objects[graphic];
|
|
99
99
|
|
|
100
100
|
if (line?.type !== "line") {
|
|
101
101
|
throw new Error(`${operation} requires an existing ${channel}-axis line.`);
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
const previous = this.guideConfigs.axis?.[
|
|
104
|
+
const previous = this.guideConfigs.axis?.[axisId]?.line;
|
|
105
105
|
const position = validateAxisPosition(
|
|
106
106
|
channel,
|
|
107
107
|
args.position ?? previous?.position ?? defaultAxisPosition(channel)
|
|
108
108
|
);
|
|
109
|
-
const scaleId = this.semanticSpec.guides.axis?.[
|
|
110
|
-
const geometry = resolveGeometry(this, channel, scaleId, position);
|
|
109
|
+
const scaleId = this.semanticSpec.guides.axis?.[axisId]?.scale;
|
|
110
|
+
const geometry = resolveGeometry(this, channel, scaleId, position, axisId);
|
|
111
111
|
const color = args.color ?? line.properties.stroke;
|
|
112
112
|
const lineWidth = args.lineWidth ?? line.properties.strokeWidth;
|
|
113
113
|
validateStyle({ color, lineWidth });
|
|
114
|
-
let next = this._withGuideConfig(
|
|
114
|
+
let next = this._withGuideConfig(axisId, "line", {
|
|
115
115
|
position,
|
|
116
116
|
color,
|
|
117
117
|
lineWidth
|
|
@@ -140,6 +140,8 @@ function createAxisLine(channel) {
|
|
|
140
140
|
{ op: operation, description: `Create the concrete ${channel}-axis line.` },
|
|
141
141
|
withGuideLayoutValidation(function (args = {}) {
|
|
142
142
|
validateKeys(args, CREATE_OPTIONS, operation);
|
|
143
|
+
const selected = selectCartesianAxis(this, channel, args, { create: true });
|
|
144
|
+
const axisId = selected.id; args = selected.options;
|
|
143
145
|
const scale = validateUserId(args.scale ?? channel, "Scale id");
|
|
144
146
|
const position = validateAxisPosition(
|
|
145
147
|
channel,
|
|
@@ -148,8 +150,8 @@ function createAxisLine(channel) {
|
|
|
148
150
|
const color = args.color ?? DEFAULT_STYLE.color;
|
|
149
151
|
const lineWidth = args.lineWidth ?? DEFAULT_STYLE.lineWidth;
|
|
150
152
|
validateStyle({ color, lineWidth });
|
|
151
|
-
resolveGeometry(this, channel, scale, position);
|
|
152
|
-
const { graphic, guidePath } = axisIds(channel);
|
|
153
|
+
resolveGeometry(this, channel, scale, position, axisId);
|
|
154
|
+
const { graphic, guidePath } = axisIds(channel, axisId);
|
|
153
155
|
|
|
154
156
|
if (this.graphicSpec.objects[graphic] !== undefined) {
|
|
155
157
|
throw new Error(`${operation} requires a missing ${channel}-axis line.`);
|
|
@@ -162,7 +164,7 @@ function createAxisLine(channel) {
|
|
|
162
164
|
type: "line",
|
|
163
165
|
...resolvePlotGraphicPlacement(this)
|
|
164
166
|
})
|
|
165
|
-
[editOperation]({ position, color, lineWidth });
|
|
167
|
+
[editOperation]({ ...axisTargetArgs(axisId, channel), position, color, lineWidth });
|
|
166
168
|
})
|
|
167
169
|
);
|
|
168
170
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { selectCartesianAxis } from "../../../selectors/axes.js";
|
|
1
2
|
import { withGuideLayoutValidation } from "../../../materialization/guides/layout.js";
|
|
2
3
|
import { action } from "../../../core/action.js";
|
|
3
4
|
import { validateKeys } from "../../../core/validation.js";
|
|
@@ -15,16 +16,19 @@ function makeRemoveAxis(channel) {
|
|
|
15
16
|
? "Theta"
|
|
16
17
|
: "Radial";
|
|
17
18
|
const operation = `remove${prefix}Axis`;
|
|
18
|
-
const graphicIds = axisGraphicIds(channel);
|
|
19
19
|
return action(
|
|
20
20
|
{
|
|
21
21
|
op: operation,
|
|
22
22
|
description: `Remove the complete ${channel}-axis resource.`
|
|
23
23
|
},
|
|
24
24
|
withGuideLayoutValidation(function (args = {}) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
25
|
+
const cartesian = ["x", "y"].includes(channel);
|
|
26
|
+
validateKeys(args, cartesian ? [...OPTIONS, "target"] : OPTIONS, operation);
|
|
27
|
+
const selected = cartesian ? selectCartesianAxis(this, channel, args, { whole: true }) : { id: channel, options: args };
|
|
28
|
+
const axisId = selected.id; args = selected.options;
|
|
29
|
+
const graphicIds = axisGraphicIds(channel, axisId);
|
|
30
|
+
const semantic = this.semanticSpec.guides.axis?.[axisId];
|
|
31
|
+
const config = this.guideConfigs.axis?.[axisId];
|
|
28
32
|
const hasGraphic = graphicIds.some(
|
|
29
33
|
id => this.graphicSpec.objects[id] !== undefined
|
|
30
34
|
);
|
|
@@ -42,7 +46,7 @@ function makeRemoveAxis(channel) {
|
|
|
42
46
|
let next = semantic === undefined
|
|
43
47
|
? this
|
|
44
48
|
: this.editSemantic({
|
|
45
|
-
property: `guide.axis.${
|
|
49
|
+
property: `guide.axis.${axisId}`,
|
|
46
50
|
remove: true
|
|
47
51
|
});
|
|
48
52
|
for (const id of graphicIds) {
|
|
@@ -50,7 +54,7 @@ function makeRemoveAxis(channel) {
|
|
|
50
54
|
next = next.editGraphics({ target: id, remove: true });
|
|
51
55
|
}
|
|
52
56
|
}
|
|
53
|
-
return next._withoutMaterializationConfig(["guides", "axis",
|
|
57
|
+
return next._withoutMaterializationConfig(["guides", "axis", axisId]);
|
|
54
58
|
})
|
|
55
59
|
);
|
|
56
60
|
}
|
|
@@ -3,7 +3,7 @@ import { action } from "../../../core/action.js";
|
|
|
3
3
|
import { isPlainObject } from "../../../core/immutable.js";
|
|
4
4
|
import { validateKeys, validateOptionObject } from "../../../core/validation.js";
|
|
5
5
|
|
|
6
|
-
const CREATE_OPTIONS = Object.freeze([
|
|
6
|
+
const CREATE_OPTIONS = Object.freeze(["target",
|
|
7
7
|
"scale",
|
|
8
8
|
"position",
|
|
9
9
|
"count",
|
|
@@ -11,14 +11,14 @@ const CREATE_OPTIONS = Object.freeze([
|
|
|
11
11
|
"ticks",
|
|
12
12
|
"labels"
|
|
13
13
|
]);
|
|
14
|
-
const EDIT_OPTIONS = Object.freeze([
|
|
14
|
+
const EDIT_OPTIONS = Object.freeze(["target",
|
|
15
15
|
"position",
|
|
16
16
|
"count",
|
|
17
17
|
"values",
|
|
18
18
|
"ticks",
|
|
19
19
|
"labels"
|
|
20
20
|
]);
|
|
21
|
-
const TICK_OPTIONS = Object.freeze(["length", "color", "lineWidth", "alongOffset"]);
|
|
21
|
+
const TICK_OPTIONS = Object.freeze(["length", "color", "lineWidth", "alongOffset", "strokeDash", "strokeDashMap"]);
|
|
22
22
|
const LABEL_OPTIONS = Object.freeze([
|
|
23
23
|
"offset",
|
|
24
24
|
"format",
|
|
@@ -34,10 +34,11 @@ const LABEL_OPTIONS = Object.freeze([
|
|
|
34
34
|
"labelMap",
|
|
35
35
|
"alongOffset",
|
|
36
36
|
"flush",
|
|
37
|
-
"colorMap"
|
|
37
|
+
"colorMap",
|
|
38
|
+
"align"
|
|
38
39
|
]);
|
|
39
|
-
const SHARED_CREATE = Object.freeze(["scale", "position", "count", "values"]);
|
|
40
|
-
const SHARED_EDIT = Object.freeze(["position", "count", "values"]);
|
|
40
|
+
const SHARED_CREATE = Object.freeze(["target", "scale", "position", "count", "values"]);
|
|
41
|
+
const SHARED_EDIT = Object.freeze(["target", "position", "count", "values"]);
|
|
41
42
|
|
|
42
43
|
function validateNested(value, supported, label) {
|
|
43
44
|
if (!isPlainObject(value)) {
|
|
@@ -104,7 +105,7 @@ function makeCreate(channel) {
|
|
|
104
105
|
const shared = select(args, SHARED_CREATE);
|
|
105
106
|
const tickArgs = { ...shared, ...(args.ticks ?? {}) };
|
|
106
107
|
const labelArgs = {
|
|
107
|
-
...select(shared, ["scale", "position"]),
|
|
108
|
+
...select(shared, ["target", "scale", "position"]),
|
|
108
109
|
...(args.labels ?? {})
|
|
109
110
|
};
|
|
110
111
|
|