ggaction 0.0.9 → 0.0.11
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 +84 -0
- package/README.md +5 -4
- package/knowledge/action-card.schema.json +1 -0
- package/knowledge/action-cards.json +1 -10941
- package/knowledge/action-cards.schema.json +1 -0
- package/knowledge/extension-authoring.md +248 -0
- package/knowledge/intent-taxonomy.json +1 -183
- package/knowledge/intent-taxonomy.schema.json +1 -0
- package/knowledge/mcp-resources.json +1 -95
- package/knowledge/mcp-resources.schema.json +1 -0
- package/knowledge/task-packet.schema.json +1 -159
- package/knowledge/task-resolver.js +1 -1230
- package/package.json +19 -2
- package/src/actions/boxPlots/edit.js +45 -298
- package/src/actions/boxPlots/options.js +46 -77
- package/src/actions/boxPlots/resolve.js +1 -1
- package/src/actions/charts/bar.js +3 -0
- package/src/actions/charts/heatmap.js +73 -83
- package/src/actions/charts/index.js +1 -1
- package/src/actions/charts/parallel.js +35 -1
- package/src/actions/charts/shared.js +20 -0
- package/src/actions/composition/actions.js +46 -49
- package/src/actions/data/bin2d.js +105 -2
- package/src/actions/data/box.js +17 -17
- package/src/actions/data/density.js +42 -78
- package/src/actions/data/gradientProfile.js +22 -52
- package/src/actions/data/horizon.js +16 -41
- package/src/actions/data/index.js +10 -10
- package/src/actions/data/interval.js +16 -38
- package/src/actions/data/intervalEdit.js +55 -0
- package/src/actions/data/regression.js +7 -17
- package/src/actions/data/shared.js +47 -0
- package/src/actions/data/timeUnit.js +14 -38
- package/src/actions/data/window.js +14 -38
- package/src/actions/distributions/revision.js +154 -34
- package/src/actions/encodings/angle.js +9 -10
- package/src/actions/encodings/appearance.js +17 -51
- package/src/actions/encodings/density/resolve.js +3 -1
- package/src/actions/encodings/density.js +1 -15
- package/src/actions/encodings/horizon/resolve.js +13 -3
- package/src/actions/encodings/horizon.js +8 -7
- package/src/actions/encodings/index.js +9 -9
- package/src/actions/encodings/offset.js +8 -24
- package/src/actions/encodings/parallel.js +9 -10
- package/src/actions/encodings/pathOrder.js +6 -13
- package/src/actions/encodings/position/policies/index.js +4 -4
- package/src/actions/encodings/position/policies/line.js +47 -17
- package/src/actions/encodings/position/resolve.js +24 -5
- package/src/actions/encodings/ranged.js +18 -23
- package/src/actions/encodings/remove.js +15 -25
- package/src/actions/encodings/ruleAppearance.js +9 -24
- package/src/actions/encodings/shared.js +37 -0
- package/src/actions/encodings/strokeDash.js +8 -23
- package/src/actions/encodings/text.js +9 -10
- package/src/actions/errorBands/create.js +105 -39
- package/src/actions/errorBands/edit.js +89 -93
- package/src/actions/errorBands/index.js +1 -2
- package/src/actions/errorBars/create.js +78 -27
- package/src/actions/errorBars/edit.js +81 -48
- package/src/actions/errorBars/index.js +1 -2
- package/src/actions/facets/actions.js +45 -69
- package/src/actions/facets/derive.js +38 -53
- package/src/actions/facets/replay.js +0 -1
- package/src/actions/gradientPlots/components.js +59 -48
- package/src/actions/gradientPlots/edit.js +35 -216
- package/src/actions/gradientPlots/index.js +11 -10
- package/src/actions/gradientPlots/materialize.js +21 -35
- package/src/actions/gradientPlots/options.js +5 -7
- package/src/actions/gradientPlots/paint.js +9 -6
- package/src/actions/gradientPlots/resolve.js +1 -1
- package/src/actions/guides/axes/index.js +3 -3
- package/src/actions/guides/axes/labels.js +68 -26
- package/src/actions/guides/axes/lines.js +22 -2
- package/src/actions/guides/axes/policy.js +28 -8
- package/src/actions/guides/axes/ticks.js +27 -9
- package/src/actions/guides/axes/titles.js +113 -65
- package/src/actions/guides/grids/resolve.js +14 -0
- package/src/actions/guides/guides.js +2 -4
- package/src/actions/guides/index.js +2 -2
- package/src/actions/guides/legends/categorical/actions.js +44 -68
- package/src/actions/guides/legends/categorical/components.js +6 -1
- package/src/actions/guides/legends/categorical/index.js +2 -2
- package/src/actions/guides/legends/categorical/layout.js +148 -175
- package/src/actions/guides/legends/categorical/recipes.js +5 -0
- package/src/actions/guides/legends/categorical/resolve.js +8 -1
- package/src/actions/guides/legends/continuous/common.js +117 -66
- package/src/actions/guides/legends/continuous/gradient.js +38 -37
- package/src/actions/guides/legends/continuous/interval.js +97 -76
- package/src/actions/guides/legends/continuous/opacity.js +28 -38
- package/src/actions/guides/legends/edit.js +14 -1
- package/src/actions/guides/legends/index.js +1 -1
- package/src/actions/guides/legends/lane.js +2 -1
- package/src/actions/guides/legends/remove.js +8 -0
- package/src/actions/guides/legends/size.js +36 -43
- package/src/actions/guides/legends/strokeWidth.js +20 -26
- package/src/actions/guides/polar/axes/labels.js +17 -1
- package/src/actions/guides/polar/axes/shared.js +7 -1
- package/src/actions/guides/polar/axes/titles.js +19 -2
- package/src/actions/guides/polar/grids.js +4 -0
- package/src/actions/guides/polar/resolve.js +6 -0
- package/src/actions/index.js +13 -13
- package/src/actions/intervals/resolve.js +87 -117
- package/src/actions/marks/arc/actions.js +31 -36
- package/src/actions/marks/area/actions.js +23 -31
- package/src/actions/marks/index.js +6 -6
- package/src/actions/marks/line/actions.js +33 -52
- package/src/actions/marks/line/materialize.js +7 -5
- package/src/actions/marks/point/materialize.js +5 -3
- package/src/actions/marks/remove.js +21 -0
- package/src/actions/marks/rule/actions.js +53 -53
- package/src/actions/marks/shared.js +8 -4
- package/src/actions/marks/text/actions.js +14 -6
- package/src/actions/marks/text/layout.js +21 -45
- package/src/actions/marks/tick/actions.js +22 -21
- package/src/actions/primitives/createGraphics.js +4 -0
- package/src/actions/primitives/editGraphics.js +4 -0
- package/src/actions/primitives/index.js +3 -3
- package/src/actions/primitives/semantic.js +1 -1
- package/src/actions/primitives/semanticValidation/index.js +2 -2
- package/src/actions/primitives/semanticValidation/layer.js +1 -1
- package/src/actions/regression/edit.js +3 -43
- package/src/actions/scales/consumers/families.js +24 -4
- package/src/actions/scales/consumers/index.js +1 -1
- package/src/actions/scales/consumers/seriesLayout.js +17 -0
- package/src/actions/scales/definitions.js +100 -103
- package/src/actions/scales/materialize.js +5 -0
- package/src/actions/selection/actions.js +79 -121
- package/src/actions/titles/actions.js +48 -94
- package/src/actions/titles/resolve.js +14 -27
- package/src/actions/violinPlots/create.js +37 -44
- package/src/core/action.js +20 -5
- package/src/core/extensionRegistry.js +104 -0
- package/src/core/textMetrics.js +74 -37
- package/src/core/validation.js +18 -0
- package/src/extension.js +8 -1
- package/src/grammar/aggregate.js +48 -28
- package/src/grammar/arcs.js +58 -11
- package/src/grammar/areaSeries.js +14 -16
- package/src/grammar/bin2d.js +37 -23
- package/src/grammar/boxPlot.js +14 -13
- package/src/grammar/categoricalDensity.js +14 -14
- package/src/grammar/categoryOrder.js +8 -14
- package/src/grammar/curveCommands.js +87 -20
- package/src/grammar/density.js +162 -141
- package/src/grammar/facets/index.js +14 -0
- package/src/grammar/facets/scales.js +7 -4
- package/src/grammar/gradientProfile.js +12 -15
- package/src/grammar/histogram.js +145 -83
- package/src/grammar/horizon.js +60 -23
- package/src/grammar/interval.js +14 -13
- package/src/grammar/lineSeries.js +91 -61
- package/src/grammar/numeric.js +290 -0
- package/src/grammar/paint.js +3 -4
- package/src/grammar/palettes.js +21 -32
- package/src/grammar/pathCommands.js +6 -0
- package/src/grammar/polarPaths.js +5 -5
- package/src/grammar/regression/derive.js +48 -9
- package/src/grammar/regression/models.js +248 -69
- package/src/grammar/regression/parameters.js +8 -0
- package/src/grammar/scales/color.js +35 -26
- package/src/grammar/scales/continuous.js +71 -24
- package/src/grammar/scales/definition.js +1 -1
- package/src/grammar/scales/discretized.js +42 -12
- package/src/grammar/scales/fields.js +21 -4
- package/src/grammar/scales/ordinal.js +117 -61
- package/src/grammar/scales/temporal.js +40 -11
- package/src/grammar/scales/transformed.js +199 -52
- package/src/grammar/schemas/graphicBounds.js +270 -79
- package/src/grammar/seriesLayout.js +50 -18
- package/src/grammar/ticks.js +123 -53
- package/src/grammar/timeUnit.js +18 -8
- package/src/grammar/transforms.js +4 -1
- package/src/grammar/window.js +70 -57
- package/src/index.js +1 -1
- package/src/layout/canvas.js +4 -8
- package/src/layout/composition.js +39 -40
- package/src/layout/facets.js +10 -14
- package/src/layout/labels.js +91 -15
- package/src/layout/legend.js +22 -20
- package/src/layout/legendLane.js +54 -40
- package/src/layout/title.js +7 -7
- package/src/materialization/bars/aggregate.js +14 -40
- package/src/materialization/bars/grouped.js +7 -18
- package/src/materialization/bars/histogram.js +2 -19
- package/src/materialization/bars/ranged.js +20 -15
- package/src/materialization/bars/resolve.js +21 -0
- package/src/materialization/facetGuides/legacyCategorical.js +2 -1
- package/src/materialization/facetGuides/placement.js +34 -19
- package/src/materialization/facetGuides/preparation.js +9 -5
- package/src/materialization/facets.js +39 -27
- package/src/materialization/guides/resources.js +2 -1
- package/src/materialization/marks/capabilities.js +5 -2
- package/src/materialization/marks/index.js +24 -4
- package/src/materialization/scales/policies/bar.js +61 -59
- package/src/materialization/scales/policies/binnedPosition.js +34 -0
- package/src/materialization/scales/policies/series.js +13 -7
- package/src/materialization/scales/resolve.js +4 -5
- package/src/materialization/selection/items/arc.js +16 -2
- package/src/materialization/selection/items/bar.js +4 -2
- package/src/materialization/selection/items/common.js +21 -1
- package/src/materialization/selection/items/path.js +19 -2
- package/src/materialization/selection/items/point.js +4 -16
- package/src/materialization/selection/items/tick.js +13 -21
- package/src/materialization/selection/point.js +11 -4
- package/src/materialization/selection/styles.js +19 -27
- package/src/materialization/text.js +4 -1
- package/src/mcp/adapter.js +8 -3
- package/src/renderers/canvas/fill.js +2 -2
- package/src/renderers/canvas/index.js +35 -2
- package/src/renderers/canvas/native.js +160 -0
- package/src/renderers/pdf.js +14 -0
- package/src/renderers/svg.js +37 -4
- package/types/extension.d.ts +19 -0
- package/types/index.d.ts +3 -0
- package/types/program.d.ts +509 -131
- package/types/svg.d.ts +1 -0
- package/src/actions/errorBands/components.js +0 -81
- package/src/actions/errorBands/options.js +0 -39
- package/src/actions/errorBands/resolve.js +0 -28
- package/src/actions/errorBars/components.js +0 -62
- package/src/actions/errorBars/options.js +0 -58
- package/src/actions/errorBars/resolve.js +0 -25
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { resolveOptionalUserId, validateUserId } from "../../core/identifiers.js";
|
|
2
2
|
import { findLayer, hasLayer } from "../../selectors/layers.js";
|
|
3
3
|
|
|
4
|
-
const CATEGORICAL = Object.freeze(["nominal", "ordinal"
|
|
4
|
+
const CATEGORICAL = Object.freeze(["nominal", "ordinal"]);
|
|
5
5
|
|
|
6
6
|
export function resolveGradientOrientation(x, y) {
|
|
7
7
|
const xType = x?.fieldType;
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { registerAxisTickGroupActions } from "./tickGroups.js";
|
|
2
|
+
import { registerCompleteAxisEditActions } from "./edit.js";
|
|
2
3
|
import { registerAxisLabelActions } from "./labels.js";
|
|
3
4
|
import { registerAxisLineActions } from "./lines.js";
|
|
4
|
-
import { registerAxisTickGroupActions } from "./tickGroups.js";
|
|
5
5
|
import { registerAxisTickActions } from "./ticks.js";
|
|
6
6
|
import { registerAxisTitleActions } from "./titles.js";
|
|
7
7
|
import { registerAxisActions } from "./axis.js";
|
|
8
|
-
import { registerCompleteAxisEditActions } from "./edit.js";
|
|
9
8
|
import {
|
|
10
9
|
removeRadialAxis,
|
|
11
10
|
removeThetaAxis,
|
|
12
11
|
removeXAxis,
|
|
13
12
|
removeYAxis
|
|
14
13
|
} from "./remove.js";
|
|
14
|
+
import { registerAxisCollectionActions } from "./axes.js";
|
|
15
15
|
import { registerParallelAxisActions } from "./parallel.js";
|
|
16
16
|
|
|
17
17
|
export function registerBasicCartesianAxisActions(ProgramClass) {
|
|
@@ -2,6 +2,7 @@ import { action } from "../../../core/action.js";
|
|
|
2
2
|
import { validateUserId } from "../../../core/identifiers.js";
|
|
3
3
|
import {
|
|
4
4
|
sameOrderedValues,
|
|
5
|
+
validateGeneratedItemLimit,
|
|
5
6
|
validateOptionObject,
|
|
6
7
|
validateNonEmptyString,
|
|
7
8
|
validateNonNegativeFinite,
|
|
@@ -27,20 +28,36 @@ import {
|
|
|
27
28
|
} from "./policy.js";
|
|
28
29
|
import { findCanvasGraphic, resolvePlotGraphicPlacement } from
|
|
29
30
|
"../../../materialization/graphicHierarchy.js";
|
|
30
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
resolveTextBounds,
|
|
33
|
+
textBoundsFitCanvas,
|
|
34
|
+
textBoundsIntersect
|
|
35
|
+
} from "../../../core/textMetrics.js";
|
|
36
|
+
import { resolveConcreteGraphicBounds } from
|
|
37
|
+
"../../../grammar/schemas/graphicBounds.js";
|
|
31
38
|
|
|
32
|
-
const OPTIONS =
|
|
39
|
+
const OPTIONS = [
|
|
33
40
|
"scale", "position", "count", "values", "offset", "format", "color",
|
|
34
41
|
"fontSize", "fontFamily", "fontWeight"
|
|
35
|
-
]
|
|
42
|
+
];
|
|
36
43
|
|
|
37
|
-
const DEFAULTS =
|
|
44
|
+
const DEFAULTS = {
|
|
38
45
|
count: 5,
|
|
39
46
|
color: DEFAULT_COLORS.text,
|
|
40
47
|
fontSize: 12,
|
|
41
48
|
fontFamily: DEFAULT_FONT_FAMILY,
|
|
42
49
|
fontWeight: "normal"
|
|
43
|
-
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export function validateAxisTextStyle(config, label) {
|
|
53
|
+
validateNonEmptyString(config.color, `${label} color`);
|
|
54
|
+
validatePositiveFinite(config.fontSize, `${label} fontSize`);
|
|
55
|
+
validateNonEmptyString(config.fontFamily, `${label} fontFamily`);
|
|
56
|
+
if (typeof config.fontWeight !== "string" &&
|
|
57
|
+
!Number.isFinite(config.fontWeight)) {
|
|
58
|
+
throw new TypeError(`${label} fontWeight must be a string or number.`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
44
61
|
|
|
45
62
|
function validateOptions(args, operation, create) {
|
|
46
63
|
validateOptionObject(
|
|
@@ -55,7 +72,10 @@ function validateOptions(args, operation, create) {
|
|
|
55
72
|
|
|
56
73
|
function validateConfig(channel, config) {
|
|
57
74
|
validateAxisPosition(channel, config.position);
|
|
58
|
-
if (config.mode === "count"
|
|
75
|
+
if (config.mode === "count") {
|
|
76
|
+
if (!Number.isInteger(config.count) || config.count <= 0) throw new RangeError("Label count must be a positive integer.");
|
|
77
|
+
validateGeneratedItemLimit(config.count, "Label count");
|
|
78
|
+
}
|
|
59
79
|
if (
|
|
60
80
|
config.mode === "values" &&
|
|
61
81
|
(!Array.isArray(config.values) || !config.values.every(value =>
|
|
@@ -64,11 +84,11 @@ function validateConfig(channel, config) {
|
|
|
64
84
|
(typeof value === "number" && Number.isFinite(value))
|
|
65
85
|
))
|
|
66
86
|
) throw new TypeError("Label values must be nominal values or finite numbers.");
|
|
87
|
+
if (config.mode === "values") {
|
|
88
|
+
validateGeneratedItemLimit(config.values.length, "Label value count");
|
|
89
|
+
}
|
|
67
90
|
validateNonNegativeFinite(config.offset, "Label offset");
|
|
68
|
-
|
|
69
|
-
validateNonEmptyString(config.color, "Label color");
|
|
70
|
-
validateNonEmptyString(config.fontFamily, "Label fontFamily");
|
|
71
|
-
if ((typeof config.fontWeight !== "string" && !Number.isFinite(config.fontWeight))) throw new TypeError("Label fontWeight must be a string or number.");
|
|
91
|
+
validateAxisTextStyle(config, "Label");
|
|
72
92
|
validateAxisFormat(config.format);
|
|
73
93
|
}
|
|
74
94
|
|
|
@@ -89,6 +109,7 @@ function resolve(program, channel, config) {
|
|
|
89
109
|
) || !bounds) throw new Error("Axis labels require a supported resolved scale and Canvas bounds.");
|
|
90
110
|
if (discrete && config.mode !== "values") throw new Error("Discrete axis labels require explicit or inferred values, not count.");
|
|
91
111
|
const values = valuesFromTickConfig(program, config);
|
|
112
|
+
validateGeneratedItemLimit(values.length, "Label value count");
|
|
92
113
|
if (discrete) {
|
|
93
114
|
const domainValues = new Set(scale.domain);
|
|
94
115
|
if (!values.every(value => domainValues.has(value))) throw new RangeError("Label values must be inside the scale domain.");
|
|
@@ -128,17 +149,29 @@ function resolve(program, channel, config) {
|
|
|
128
149
|
y: Array.isArray(resolved.y) ? resolved.y[index] : resolved.y,
|
|
129
150
|
text: value,
|
|
130
151
|
fontSize: config.fontSize,
|
|
152
|
+
fontFamily: config.fontFamily,
|
|
153
|
+
fontWeight: config.fontWeight,
|
|
131
154
|
textAlign: resolved.textAlign,
|
|
132
155
|
textBaseline: resolved.textBaseline
|
|
133
156
|
}));
|
|
134
|
-
const
|
|
135
|
-
?
|
|
136
|
-
:
|
|
137
|
-
|
|
138
|
-
: true;
|
|
139
|
-
if (!canvas || !fits) {
|
|
157
|
+
const orderedBounds = [...labelBounds].sort((left, right) => channel === "x"
|
|
158
|
+
? left.left - right.left
|
|
159
|
+
: left.top - right.top);
|
|
160
|
+
if (!canvas || !labelBounds.every(item => textBoundsFitCanvas(item, canvas))) {
|
|
140
161
|
throw new Error(`The ${channel}-axis labels do not fit the Canvas margin.`);
|
|
141
162
|
}
|
|
163
|
+
if (orderedBounds.some((item, index) => index > 0 &&
|
|
164
|
+
textBoundsIntersect(orderedBounds[index - 1], item))) {
|
|
165
|
+
throw new Error(`The ${channel}-axis labels overlap each other.`);
|
|
166
|
+
}
|
|
167
|
+
const title = program.graphicSpec.objects[`${channel}AxisTitle`]
|
|
168
|
+
? resolveConcreteGraphicBounds(program.graphicSpec, `${channel}AxisTitle`)
|
|
169
|
+
: undefined;
|
|
170
|
+
if (title &&
|
|
171
|
+
program.guideConfigs.axis?.[channel]?.title?.inferredOffset !== true &&
|
|
172
|
+
labelBounds.some(item => textBoundsIntersect(item, title))) {
|
|
173
|
+
throw new Error(`The ${channel}-axis labels overlap the axis title.`);
|
|
174
|
+
}
|
|
142
175
|
return resolved;
|
|
143
176
|
}
|
|
144
177
|
|
|
@@ -170,16 +203,25 @@ function makeEdit(channel) {
|
|
|
170
203
|
validateConfig(channel, config);
|
|
171
204
|
assertTickCompatibility(this.guideConfigs.axis?.[channel]?.ticks, config, op);
|
|
172
205
|
const resolved = resolve(this, channel, config);
|
|
173
|
-
let next = this._withGuideConfig(channel, "labels", config)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
206
|
+
let next = this._withGuideConfig(channel, "labels", config);
|
|
207
|
+
for (const [property, value] of Object.entries({
|
|
208
|
+
length: resolved.values.length,
|
|
209
|
+
x: resolved.x,
|
|
210
|
+
y: resolved.y,
|
|
211
|
+
text: resolved.text,
|
|
212
|
+
fill: config.color,
|
|
213
|
+
fontSize: config.fontSize,
|
|
214
|
+
fontFamily: config.fontFamily,
|
|
215
|
+
fontWeight: config.fontWeight,
|
|
216
|
+
textAlign: resolved.textAlign,
|
|
217
|
+
textBaseline: resolved.textBaseline
|
|
218
|
+
})) next = next.editGraphics({ target: id, property, value });
|
|
219
|
+
const titleConfig = next.guideConfigs.axis?.[channel]?.title;
|
|
220
|
+
if (titleConfig?.inferredOffset === true) {
|
|
221
|
+
const editTitle = channel === "x" ? "editXAxisTitle" : "editYAxisTitle";
|
|
222
|
+
next = next[editTitle]();
|
|
223
|
+
}
|
|
224
|
+
return next;
|
|
183
225
|
});
|
|
184
226
|
}
|
|
185
227
|
|
|
@@ -24,6 +24,20 @@ function validateStyle({ color, lineWidth }) {
|
|
|
24
24
|
validateNonNegativeFinite(lineWidth, "Axis lineWidth");
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
function resolveLineRange(resolvedScale, bounds, channel) {
|
|
28
|
+
const range = resolvedScale?.range;
|
|
29
|
+
if (
|
|
30
|
+
resolvedScale?.type !== "time" ||
|
|
31
|
+
!Number.isFinite(resolvedScale.bandwidth)
|
|
32
|
+
) return range;
|
|
33
|
+
const plotRange = channel === "x"
|
|
34
|
+
? [bounds.x, bounds.x + bounds.width]
|
|
35
|
+
: [bounds.y + bounds.height, bounds.y];
|
|
36
|
+
return Math.sign(range[1] - range[0]) === Math.sign(plotRange[1] - plotRange[0])
|
|
37
|
+
? plotRange
|
|
38
|
+
: plotRange.reverse();
|
|
39
|
+
}
|
|
40
|
+
|
|
27
41
|
function resolveGeometry(program, channel, scaleId, position) {
|
|
28
42
|
const hasConsumer = program.semanticSpec.layers.some(
|
|
29
43
|
layer => layer.encoding?.[channel]?.scale === scaleId
|
|
@@ -35,7 +49,8 @@ function resolveGeometry(program, channel, scaleId, position) {
|
|
|
35
49
|
);
|
|
36
50
|
}
|
|
37
51
|
|
|
38
|
-
const
|
|
52
|
+
const resolvedScale = program.resolvedScales[scaleId];
|
|
53
|
+
const range = resolvedScale?.range;
|
|
39
54
|
const bounds = resolveGraphicBounds(program);
|
|
40
55
|
|
|
41
56
|
if (!Array.isArray(range) || range.length !== 2 || !range.every(Number.isFinite)) {
|
|
@@ -49,7 +64,12 @@ function resolveGeometry(program, channel, scaleId, position) {
|
|
|
49
64
|
throw new Error("Axis line requires graphical Canvas bounds.");
|
|
50
65
|
}
|
|
51
66
|
|
|
52
|
-
return resolveAxisLineGeometry(
|
|
67
|
+
return resolveAxisLineGeometry(
|
|
68
|
+
bounds,
|
|
69
|
+
channel,
|
|
70
|
+
position,
|
|
71
|
+
resolveLineRange(resolvedScale, bounds, channel)
|
|
72
|
+
);
|
|
53
73
|
}
|
|
54
74
|
|
|
55
75
|
function axisIds(channel) {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { formatVisibleText } from "../../../core/textMetrics.js";
|
|
2
|
+
|
|
1
3
|
const AXIS_POSITIONS = Object.freeze({
|
|
2
4
|
x: Object.freeze(["bottom", "top"]),
|
|
3
5
|
y: Object.freeze(["left", "right"])
|
|
@@ -6,7 +8,24 @@ const AXIS_POSITIONS = Object.freeze({
|
|
|
6
8
|
const NUMERIC_FORMATS = new Set([
|
|
7
9
|
".0f", ".1f", ".2f", ".0%", ".1%", ".2e"
|
|
8
10
|
]);
|
|
9
|
-
const
|
|
11
|
+
const TIME_DIRECTIVES = new Set(["Y", "m", "d", "b", "%"]);
|
|
12
|
+
const MONTH_NAMES = Object.freeze([
|
|
13
|
+
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
|
14
|
+
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
function isTimeFormat(format) {
|
|
18
|
+
if (typeof format !== "string") return false;
|
|
19
|
+
let directives = 0;
|
|
20
|
+
for (let index = 0; index < format.length; index += 1) {
|
|
21
|
+
if (format[index] !== "%") continue;
|
|
22
|
+
const directive = format[index + 1];
|
|
23
|
+
if (!TIME_DIRECTIVES.has(directive)) return false;
|
|
24
|
+
if (directive !== "%") directives += 1;
|
|
25
|
+
index += 1;
|
|
26
|
+
}
|
|
27
|
+
return directives > 0;
|
|
28
|
+
}
|
|
10
29
|
|
|
11
30
|
export function defaultAxisPosition(channel) {
|
|
12
31
|
return channel === "x" ? "bottom" : "left";
|
|
@@ -105,7 +124,7 @@ export function validateAxisFormat(format) {
|
|
|
105
124
|
return format;
|
|
106
125
|
}
|
|
107
126
|
if (typeof format === "string" && (
|
|
108
|
-
NUMERIC_FORMATS.has(format) ||
|
|
127
|
+
NUMERIC_FORMATS.has(format) || isTimeFormat(format)
|
|
109
128
|
)) {
|
|
110
129
|
return format;
|
|
111
130
|
}
|
|
@@ -126,24 +145,25 @@ function formatTime(value, format) {
|
|
|
126
145
|
const year = pad(date.getUTCFullYear(), 4);
|
|
127
146
|
const month = pad(date.getUTCMonth() + 1);
|
|
128
147
|
const day = pad(date.getUTCDate());
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
148
|
+
const values = { Y: year, m: month, d: day, b: MONTH_NAMES[date.getUTCMonth()] };
|
|
149
|
+
return format.replace(/%([Ymdb%])/g, (_, directive) =>
|
|
150
|
+
directive === "%" ? "%" : values[directive]
|
|
151
|
+
);
|
|
132
152
|
}
|
|
133
153
|
|
|
134
154
|
export function formatAxisValue(value, scaleType, format, autoFormatter) {
|
|
135
155
|
validateAxisFormat(format);
|
|
136
|
-
if (format === "auto") return autoFormatter(value);
|
|
156
|
+
if (format === "auto") return formatVisibleText(autoFormatter(value));
|
|
137
157
|
if (["ordinal", "band", "point"].includes(scaleType)) {
|
|
138
158
|
throw new Error('Discrete axis labels require format "auto".');
|
|
139
159
|
}
|
|
140
160
|
if (scaleType === "time") {
|
|
141
|
-
if (!
|
|
161
|
+
if (!isTimeFormat(format)) {
|
|
142
162
|
throw new Error("Time axis labels require a supported time format string.");
|
|
143
163
|
}
|
|
144
164
|
return formatTime(value, format);
|
|
145
165
|
}
|
|
146
|
-
if (
|
|
166
|
+
if (isTimeFormat(format)) {
|
|
147
167
|
throw new Error("Quantitative axis labels cannot use a time format string.");
|
|
148
168
|
}
|
|
149
169
|
if (typeof format === "object") return value.toFixed(format.decimals);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { action } from "../../../core/action.js";
|
|
2
2
|
import { validateUserId } from "../../../core/identifiers.js";
|
|
3
3
|
import {
|
|
4
|
+
validateGeneratedItemLimit,
|
|
4
5
|
validateNonEmptyString,
|
|
5
6
|
validateNonNegativeFinite,
|
|
6
7
|
validateOptionObject
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
mapContinuousScaleValues,
|
|
12
13
|
mapOrdinalPositionValues
|
|
13
14
|
} from "../../../grammar/scales/index.js";
|
|
15
|
+
import { numericExtent } from "../../../grammar/numeric.js";
|
|
14
16
|
import {
|
|
15
17
|
DEFAULT_TICK_COUNT,
|
|
16
18
|
inferHistogramBoundaries,
|
|
@@ -40,11 +42,17 @@ function validateOptions(args, operation, create) {
|
|
|
40
42
|
operation
|
|
41
43
|
);
|
|
42
44
|
if (Object.hasOwn(args, "count") && Object.hasOwn(args, "values")) throw new Error(`${operation} cannot use count and values together.`);
|
|
45
|
+
if (Array.isArray(args.values)) {
|
|
46
|
+
validateGeneratedItemLimit(args.values.length, "Tick value count");
|
|
47
|
+
}
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
function validateConfig(channel, config) {
|
|
46
51
|
validateAxisPosition(channel, config.position);
|
|
47
|
-
if (config.mode === "count"
|
|
52
|
+
if (config.mode === "count") {
|
|
53
|
+
if (!Number.isInteger(config.count) || config.count <= 0) throw new RangeError("Tick count must be a positive integer.");
|
|
54
|
+
validateGeneratedItemLimit(config.count, "Tick count");
|
|
55
|
+
}
|
|
48
56
|
if (
|
|
49
57
|
config.mode === "values" &&
|
|
50
58
|
(!Array.isArray(config.values) || !config.values.every(value =>
|
|
@@ -69,6 +77,7 @@ function geometry(program, channel, config) {
|
|
|
69
77
|
if (discrete && config.mode !== "values") throw new Error("Discrete axis ticks require explicit or inferred values, not count.");
|
|
70
78
|
const domain = scale.domain;
|
|
71
79
|
const values = valuesFromTickConfig(program, config);
|
|
80
|
+
validateGeneratedItemLimit(values.length, "Tick value count");
|
|
72
81
|
if (discrete) {
|
|
73
82
|
const domainValues = new Set(domain);
|
|
74
83
|
if (!values.every(value => domainValues.has(value))) throw new RangeError("Tick values must be inside the scale domain.");
|
|
@@ -90,16 +99,25 @@ function geometry(program, channel, config) {
|
|
|
90
99
|
})
|
|
91
100
|
};
|
|
92
101
|
const canvas = findCanvasGraphic(program)?.properties;
|
|
93
|
-
const
|
|
94
|
-
? [...resolved.x1, ...resolved.x2
|
|
95
|
-
: [resolved.x1, resolved.x2
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
102
|
+
const xCoordinates = channel === "x"
|
|
103
|
+
? [...resolved.x1, ...resolved.x2]
|
|
104
|
+
: [resolved.x1, resolved.x2];
|
|
105
|
+
const yCoordinates = channel === "x"
|
|
106
|
+
? [resolved.y1, resolved.y2]
|
|
107
|
+
: [...resolved.y1, ...resolved.y2];
|
|
108
|
+
const coordinates = [...xCoordinates, ...yCoordinates];
|
|
109
|
+
const [left, right] = numericExtent(xCoordinates);
|
|
110
|
+
const [top, bottom] = numericExtent(yCoordinates);
|
|
111
|
+
const strokeExtent = config.lineWidth / 2;
|
|
112
|
+
const epsilon = 1e-9;
|
|
113
|
+
const fits = canvas &&
|
|
114
|
+
left - strokeExtent >= -epsilon &&
|
|
115
|
+
right + strokeExtent <= canvas.width + epsilon &&
|
|
116
|
+
top - strokeExtent >= -epsilon &&
|
|
117
|
+
bottom + strokeExtent <= canvas.height + epsilon;
|
|
99
118
|
if (
|
|
100
|
-
!canvas ||
|
|
101
119
|
coordinates.some(value => !Number.isFinite(value)) ||
|
|
102
|
-
|
|
120
|
+
!fits
|
|
103
121
|
) {
|
|
104
122
|
throw new Error(`The ${channel}-axis ticks do not fit the Canvas margin.`);
|
|
105
123
|
}
|
|
@@ -3,8 +3,7 @@ import { validateUserId } from "../../../core/identifiers.js";
|
|
|
3
3
|
import {
|
|
4
4
|
validateKeys,
|
|
5
5
|
validateNonEmptyString,
|
|
6
|
-
validateNonNegativeFinite
|
|
7
|
-
validatePositiveFinite
|
|
6
|
+
validateNonNegativeFinite
|
|
8
7
|
} from "../../../core/validation.js";
|
|
9
8
|
import { resolveGraphicBounds } from "../../../layout/canvas.js";
|
|
10
9
|
import {
|
|
@@ -16,6 +15,7 @@ import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
|
|
|
16
15
|
"../../../theme/defaults.js";
|
|
17
16
|
import { findDataset } from "../../../selectors/datasets.js";
|
|
18
17
|
import { formatAggregateTitle } from "../../../grammar/aggregate.js";
|
|
18
|
+
import { finiteMidpoint } from "../../../grammar/numeric.js";
|
|
19
19
|
import {
|
|
20
20
|
defaultAxisPosition,
|
|
21
21
|
defaultAxisTitleRotation,
|
|
@@ -24,19 +24,67 @@ import {
|
|
|
24
24
|
} from "./policy.js";
|
|
25
25
|
import { findCanvasGraphic, resolvePlotGraphicPlacement } from
|
|
26
26
|
"../../../materialization/graphicHierarchy.js";
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
resolveTextBounds,
|
|
29
|
+
textBoundsFitCanvas,
|
|
30
|
+
textBoundsIntersect
|
|
31
|
+
} from "../../../core/textMetrics.js";
|
|
32
|
+
import { resolveConcreteGraphicBounds } from
|
|
33
|
+
"../../../grammar/schemas/graphicBounds.js";
|
|
34
|
+
import { validateAxisTextStyle } from "./labels.js";
|
|
35
|
+
|
|
36
|
+
function titleBounds(geometry, config, text) {
|
|
37
|
+
return resolveTextBounds({
|
|
38
|
+
...geometry,
|
|
39
|
+
text,
|
|
40
|
+
fontSize: config.fontSize,
|
|
41
|
+
fontFamily: config.fontFamily,
|
|
42
|
+
fontWeight: config.fontWeight,
|
|
43
|
+
textAlign: "center",
|
|
44
|
+
textBaseline: "middle",
|
|
45
|
+
rotation: config.rotation
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function inferredTitleOffset(program, channel, config) {
|
|
50
|
+
const plot = resolveGraphicBounds(program);
|
|
51
|
+
const canvas = findCanvasGraphic(program)?.properties;
|
|
52
|
+
const labels = program.graphicSpec.objects[`${channel}AxisLabels`]
|
|
53
|
+
? resolveConcreteGraphicBounds(program.graphicSpec, `${channel}AxisLabels`)
|
|
54
|
+
: undefined;
|
|
55
|
+
const preferred = channel === "x" ? 42 : 52;
|
|
56
|
+
if (!plot || !canvas || !labels) return preferred;
|
|
57
|
+
const horizontal = channel === "x";
|
|
58
|
+
const positive = config.position === (horizontal ? "bottom" : "right");
|
|
59
|
+
const near = horizontal ? "top" : "left";
|
|
60
|
+
const far = horizontal ? "bottom" : "right";
|
|
61
|
+
const bounds = titleBounds(resolveAxisTitleGeometry({
|
|
62
|
+
bounds: plot,
|
|
63
|
+
channel,
|
|
64
|
+
position: config.position,
|
|
65
|
+
along: 0,
|
|
66
|
+
offset: preferred
|
|
67
|
+
}), config, program.semanticSpec.guides.axis?.[channel]?.title ?? "");
|
|
68
|
+
const needed = positive
|
|
69
|
+
? labels[far] + 4 - bounds[near]
|
|
70
|
+
: bounds[far] - labels[near] + 4;
|
|
71
|
+
const available = positive
|
|
72
|
+
? canvas[horizontal ? "height" : "width"] - bounds[far]
|
|
73
|
+
: bounds[near];
|
|
74
|
+
return preferred + Math.min(available, Math.max(0, needed));
|
|
75
|
+
}
|
|
28
76
|
|
|
29
|
-
const CREATE_OPTIONS =
|
|
77
|
+
const CREATE_OPTIONS = [
|
|
30
78
|
"text", "scale", "position", "at", "offset", "rotation", "color",
|
|
31
79
|
"fontSize", "fontFamily", "fontWeight"
|
|
32
|
-
]
|
|
80
|
+
];
|
|
33
81
|
const EDIT_OPTIONS = CREATE_OPTIONS.filter(key => key !== "scale");
|
|
34
|
-
const DEFAULTS =
|
|
82
|
+
const DEFAULTS = {
|
|
35
83
|
color: DEFAULT_COLORS.text,
|
|
36
84
|
fontSize: 13,
|
|
37
85
|
fontFamily: DEFAULT_FONT_FAMILY,
|
|
38
86
|
fontWeight: 600
|
|
39
|
-
}
|
|
87
|
+
};
|
|
40
88
|
|
|
41
89
|
function validateText(text) {
|
|
42
90
|
return validateNonEmptyString(text, "Axis title text");
|
|
@@ -47,10 +95,7 @@ function validateConfig(channel, config) {
|
|
|
47
95
|
if (!["start", "center", "end"].includes(config.at) && !Number.isFinite(config.at)) throw new TypeError("Axis title at must be start, center, end, or a finite number.");
|
|
48
96
|
validateNonNegativeFinite(config.offset, "Axis title offset");
|
|
49
97
|
if (!Number.isFinite(config.rotation)) throw new TypeError("Axis title rotation must be finite radians.");
|
|
50
|
-
|
|
51
|
-
validatePositiveFinite(config.fontSize, "Axis title fontSize");
|
|
52
|
-
validateNonEmptyString(config.fontFamily, "Axis title fontFamily");
|
|
53
|
-
if (typeof config.fontWeight !== "string" && !Number.isFinite(config.fontWeight)) throw new TypeError("Axis title fontWeight must be a string or number.");
|
|
98
|
+
validateAxisTextStyle(config, "Axis title");
|
|
54
99
|
}
|
|
55
100
|
|
|
56
101
|
export function inferAxisTitleText(program, channel, scaleId) {
|
|
@@ -64,34 +109,24 @@ export function inferAxisTitleText(program, channel, scaleId) {
|
|
|
64
109
|
encoding.field.length
|
|
65
110
|
) {
|
|
66
111
|
const dataset = findDataset(program, layer.data);
|
|
67
|
-
const
|
|
68
|
-
dataset.transform[0].type === "density"
|
|
112
|
+
const transform = dataset?.transform?.length === 1
|
|
69
113
|
? dataset.transform[0]
|
|
70
114
|
: undefined;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
?
|
|
75
|
-
: encoding.field ===
|
|
115
|
+
let derivedTitle;
|
|
116
|
+
if (transform?.type === "density") {
|
|
117
|
+
derivedTitle = encoding.field === transform.as?.[0]
|
|
118
|
+
? transform.field
|
|
119
|
+
: encoding.field === transform.as?.[1]
|
|
76
120
|
? "Density"
|
|
77
121
|
: undefined;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const box = dataset?.transform?.length === 1 &&
|
|
87
|
-
dataset.transform[0].type === "boxSummary"
|
|
88
|
-
? dataset.transform[0]
|
|
89
|
-
: undefined;
|
|
90
|
-
const boxTitle = box !== undefined &&
|
|
91
|
-
Object.values(box.as).includes(encoding.field)
|
|
92
|
-
? box.field
|
|
93
|
-
: undefined;
|
|
94
|
-
const title = encoding.title ?? densityTitle ?? intervalTitle ?? boxTitle ?? (encoding.aggregate === undefined
|
|
122
|
+
} else if (transform?.type === "interval" &&
|
|
123
|
+
Object.values(transform.as).includes(encoding.field)) {
|
|
124
|
+
derivedTitle = `${transform.center}(${transform.field})`;
|
|
125
|
+
} else if (transform?.type === "boxSummary" &&
|
|
126
|
+
Object.values(transform.as).includes(encoding.field)) {
|
|
127
|
+
derivedTitle = transform.field;
|
|
128
|
+
}
|
|
129
|
+
const title = encoding.title ?? derivedTitle ?? (encoding.aggregate === undefined
|
|
95
130
|
? encoding.field
|
|
96
131
|
: formatAggregateTitle(encoding.aggregate, encoding.field));
|
|
97
132
|
titles.add(title);
|
|
@@ -112,10 +147,11 @@ function resolveGeometry(program, channel, config) {
|
|
|
112
147
|
!isTransformedScaleType(scale?.type)
|
|
113
148
|
) || !bounds) throw new Error("Axis title requires a supported resolved scale and Canvas bounds.");
|
|
114
149
|
let along;
|
|
115
|
-
if (config.at === "
|
|
116
|
-
|
|
117
|
-
else if (config.at === "end")
|
|
118
|
-
|
|
150
|
+
if (config.at === "center") {
|
|
151
|
+
along = finiteMidpoint(scale.range[0], scale.range[1]);
|
|
152
|
+
} else if (config.at === "start" || config.at === "end") {
|
|
153
|
+
along = scale.range[config.at === "start" ? 0 : 1];
|
|
154
|
+
} else {
|
|
119
155
|
if (discrete) {
|
|
120
156
|
if (!scale.domain.includes(config.at)) throw new RangeError("Axis title at value must be inside the scale domain.");
|
|
121
157
|
along = mapOrdinalPositionValues([config.at], scale)[0];
|
|
@@ -132,24 +168,21 @@ function resolveGeometry(program, channel, config) {
|
|
|
132
168
|
along,
|
|
133
169
|
offset: config.offset
|
|
134
170
|
});
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
textAlign: "center",
|
|
141
|
-
textBaseline: "middle",
|
|
142
|
-
rotation: config.rotation
|
|
143
|
-
});
|
|
171
|
+
const resolvedBounds = titleBounds(
|
|
172
|
+
geometry,
|
|
173
|
+
config,
|
|
174
|
+
program.semanticSpec.guides.axis?.[channel]?.title ?? ""
|
|
175
|
+
);
|
|
144
176
|
const canvas = findCanvasGraphic(program)?.properties;
|
|
145
|
-
|
|
146
|
-
? titleBounds.top >= 0
|
|
147
|
-
: config.position === "right"
|
|
148
|
-
? titleBounds.right <= canvas?.width
|
|
149
|
-
: true;
|
|
150
|
-
if (!canvas || !newEdgeFits) {
|
|
177
|
+
if (!canvas || !textBoundsFitCanvas(resolvedBounds, canvas)) {
|
|
151
178
|
throw new Error(`The ${channel}-axis title does not fit the Canvas margin.`);
|
|
152
179
|
}
|
|
180
|
+
const labelsBounds = program.graphicSpec.objects[`${channel}AxisLabels`]
|
|
181
|
+
? resolveConcreteGraphicBounds(program.graphicSpec, `${channel}AxisLabels`)
|
|
182
|
+
: undefined;
|
|
183
|
+
if (labelsBounds && textBoundsIntersect(resolvedBounds, labelsBounds)) {
|
|
184
|
+
throw new Error(`The ${channel}-axis title overlaps the axis labels.`);
|
|
185
|
+
}
|
|
153
186
|
return geometry;
|
|
154
187
|
}
|
|
155
188
|
|
|
@@ -180,6 +213,9 @@ function makeEdit(channel) {
|
|
|
180
213
|
? defaultAxisTitleRotation(channel, position)
|
|
181
214
|
: appearance.rotation ?? previous.rotation,
|
|
182
215
|
inferredRotation,
|
|
216
|
+
inferredOffset: Object.hasOwn(args, "offset")
|
|
217
|
+
? false
|
|
218
|
+
: previous.inferredOffset === true,
|
|
183
219
|
...(explicitText ? { inferredText: false } : {})
|
|
184
220
|
};
|
|
185
221
|
validateConfig(channel, config);
|
|
@@ -196,6 +232,9 @@ function makeEdit(channel) {
|
|
|
196
232
|
});
|
|
197
233
|
}
|
|
198
234
|
const resolvedText = validateText(next.semanticSpec.guides.axis?.[channel]?.title);
|
|
235
|
+
if (config.inferredOffset) {
|
|
236
|
+
config.offset = inferredTitleOffset(next, channel, config);
|
|
237
|
+
}
|
|
199
238
|
const geometry = resolveGeometry(next, channel, config);
|
|
200
239
|
next = next._withGuideConfig(channel, "title", config);
|
|
201
240
|
const properties = {
|
|
@@ -216,16 +255,24 @@ function makeCreate(channel) {
|
|
|
216
255
|
const operation = names(channel);
|
|
217
256
|
return action({ op: operation.create, description: `Create the ${channel}-axis title.` }, function (args = {}) {
|
|
218
257
|
validateKeys(args, CREATE_OPTIONS, operation.create);
|
|
219
|
-
const
|
|
258
|
+
const {
|
|
259
|
+
text: requestedText,
|
|
260
|
+
scale: requestedScale,
|
|
261
|
+
position: requestedPosition,
|
|
262
|
+
rotation: requestedRotation,
|
|
263
|
+
...appearance
|
|
264
|
+
} = args;
|
|
265
|
+
const scale = validateUserId(requestedScale ?? channel, "Scale id");
|
|
220
266
|
const guideScale = this.semanticSpec.guides.axis?.[channel]?.scale;
|
|
221
267
|
if (guideScale && guideScale !== scale) throw new Error(`${operation.create} conflicts with the existing axis scale.`);
|
|
222
268
|
if (this.graphicSpec.objects[operation.graphic]) throw new Error(`${operation.create} requires a missing axis title.`);
|
|
223
269
|
const inferredText = !Object.hasOwn(args, "text");
|
|
224
270
|
const text = validateText(
|
|
225
|
-
|
|
271
|
+
requestedText ?? inferAxisTitleText(this, channel, scale)
|
|
226
272
|
);
|
|
227
|
-
const position =
|
|
273
|
+
const position = requestedPosition ?? defaultAxisPosition(channel);
|
|
228
274
|
const inferredRotation = !Object.hasOwn(args, "rotation");
|
|
275
|
+
const inferredOffset = !Object.hasOwn(args, "offset");
|
|
229
276
|
const config = {
|
|
230
277
|
scale,
|
|
231
278
|
inferredText,
|
|
@@ -234,21 +281,22 @@ function makeCreate(channel) {
|
|
|
234
281
|
offset: channel === "x" ? 42 : 52,
|
|
235
282
|
rotation: inferredRotation
|
|
236
283
|
? defaultAxisTitleRotation(channel, position)
|
|
237
|
-
:
|
|
284
|
+
: requestedRotation,
|
|
238
285
|
inferredRotation,
|
|
286
|
+
inferredOffset,
|
|
239
287
|
color: DEFAULTS.color,
|
|
240
288
|
fontSize: DEFAULTS.fontSize,
|
|
241
289
|
fontFamily: DEFAULTS.fontFamily,
|
|
242
290
|
fontWeight: DEFAULTS.fontWeight,
|
|
243
|
-
...
|
|
244
|
-
([key]) => !["text", "scale", "position", "rotation"].includes(key)
|
|
245
|
-
))
|
|
291
|
+
...appearance
|
|
246
292
|
};
|
|
247
|
-
|
|
248
|
-
resolveGeometry(this, channel, config);
|
|
249
|
-
return this
|
|
293
|
+
let next = this
|
|
250
294
|
.editSemantic({ property: `guide.axis.${channel}.scale`, value: scale })
|
|
251
|
-
.editSemantic({ property: `guide.axis.${channel}.title`, value: text })
|
|
295
|
+
.editSemantic({ property: `guide.axis.${channel}.title`, value: text });
|
|
296
|
+
if (inferredOffset) config.offset = inferredTitleOffset(next, channel, config);
|
|
297
|
+
validateConfig(channel, config);
|
|
298
|
+
resolveGeometry(next, channel, config);
|
|
299
|
+
return next
|
|
252
300
|
.createGraphics({
|
|
253
301
|
id: operation.graphic,
|
|
254
302
|
type: "text",
|