ggaction 0.0.23 → 0.0.24
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 +18 -0
- package/README.md +2 -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/charts/heatmap.js +1 -1
- package/src/actions/data/basicBin2d.js +1 -1
- package/src/actions/data/bin2d.js +2 -2
- package/src/actions/encodings/ranged.js +2 -2
- package/src/actions/facets/actions.js +1 -1
- package/src/actions/guides/axes/components.js +5 -0
- package/src/actions/guides/axes/edit.js +1 -188
- package/src/actions/guides/axes/labels.js +1 -537
- package/src/actions/guides/axes/tickGroups.js +16 -1
- package/src/actions/guides/axes/ticks.js +33 -24
- package/src/actions/guides/axes/titles.js +1 -389
- package/src/actions/titles/resolve.js +11 -1
- package/src/core/compositionState.js +4 -1
- package/src/core/facetGap.js +44 -0
- package/src/grammar/axisTextPlacement.js +1 -1
- package/src/grammar/bin2d.js +34 -4
- package/src/inspectionDescriptors.js +1 -1
- package/src/layout/composition.js +2 -6
- package/src/layout/facetContentFlow.js +15 -7
- package/src/layout/facets.js +1 -1
- package/src/layout/title.js +1 -1
- package/src/materialization/guides/layout.js +1 -1
- package/src/materialization/guides/resources.js +1 -1
- package/src/materialization/planner.js +19 -4
- package/src/persistence/validation.js +19 -1
- package/src/version.js +1 -1
- package/types/basic.d.ts +2 -0
- package/types/index.d.ts +3 -0
- package/types/program.d.ts +19 -7
|
@@ -1,537 +1 @@
|
|
|
1
|
-
import { axisTitleAllowsLabelOverlap } from "../../../grammar/axisTextPlacement.js";
|
|
2
|
-
import { selectCartesianAxis, axisTargetArgs } from "../../../selectors/axes.js";
|
|
3
|
-
import { axisGraphicIds } from "../../../materialization/guides/resources.js";
|
|
4
|
-
import { axisThemeTypography } from "../../theme/state.js";
|
|
5
|
-
import { validateAxisTextPlacement } from "../../../grammar/axisTextPlacement.js";
|
|
6
|
-
import { canvasOverflowError } from "../../../layout/canvas.js";
|
|
7
|
-
import { withGuideLayoutValidation } from "../../../materialization/guides/layout.js";
|
|
8
|
-
import { action } from "../../../core/action.js";
|
|
9
|
-
import { validateUserId } from "../../../core/identifiers.js";
|
|
10
|
-
import {
|
|
11
|
-
sameOrderedValues,
|
|
12
|
-
validateGeneratedItemLimit,
|
|
13
|
-
validateTextStyle,
|
|
14
|
-
validateOptionObject,
|
|
15
|
-
validateNonNegativeFinite,
|
|
16
|
-
validatePositiveFinite
|
|
17
|
-
} from "../../../core/validation.js";
|
|
18
|
-
import { resolveCartesianAxisFrameBounds } from "../../../materialization/coordinateBounds.js";
|
|
19
|
-
import {
|
|
20
|
-
isTransformedScaleType,
|
|
21
|
-
mapContinuousScaleValues,
|
|
22
|
-
formatTransformedTick,
|
|
23
|
-
mapOrdinalPositionValues
|
|
24
|
-
} from "../../../grammar/scales/index.js";
|
|
25
|
-
import { formatTimeTick, formatTimeTicks, validateTickRounding } from "../../../grammar/ticks.js";
|
|
26
|
-
import { resolveRotation } from "../../../grammar/rotation.js";
|
|
27
|
-
import { valuesFromTickConfig } from "../tickValues.js";
|
|
28
|
-
import { AXIS_LABEL_FONT_SIZES, DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
|
|
29
|
-
"../../../theme/defaults.js";
|
|
30
|
-
import {
|
|
31
|
-
defaultAxisPosition,
|
|
32
|
-
formatAxisValue,
|
|
33
|
-
formatNumericAxisTicks,
|
|
34
|
-
resolveAxisLabelGeometry,
|
|
35
|
-
validateAxisFormat,
|
|
36
|
-
validateAxisPosition
|
|
37
|
-
} from "./policy.js";
|
|
38
|
-
import { findCanvasGraphic, resolvePlotGraphicPlacement } from
|
|
39
|
-
"../../../materialization/graphicHierarchy.js";
|
|
40
|
-
import {
|
|
41
|
-
resolveTextBounds,
|
|
42
|
-
textBoundsFitCanvas,
|
|
43
|
-
textBoundsIntersect
|
|
44
|
-
} from "../../../core/textMetrics.js";
|
|
45
|
-
import { resolveConcreteGraphicBounds } from
|
|
46
|
-
"../../../grammar/schemas/graphicBounds.js";
|
|
47
|
-
import { wrapText } from "../../../layout/text.js";
|
|
48
|
-
import {
|
|
49
|
-
normalizeDisplayLabelMap,
|
|
50
|
-
normalizeDisplayColorMap,
|
|
51
|
-
resolveDisplayLabelColor,
|
|
52
|
-
resolveDisplayLabel
|
|
53
|
-
} from "../../../grammar/displayLabels.js";
|
|
54
|
-
|
|
55
|
-
const OPTIONS = [
|
|
56
|
-
"target", "scale", "position", "count", "values", "rounding", "offset", "format", "color",
|
|
57
|
-
"fontSize", "fontFamily", "fontWeight", "rotation", "maxWidth", "wrap",
|
|
58
|
-
"lineHeight", "overlap", "labelMap", "alongOffset", "flush", "colorMap", "align"
|
|
59
|
-
];
|
|
60
|
-
|
|
61
|
-
const DEFAULTS = {
|
|
62
|
-
count: 5,
|
|
63
|
-
color: DEFAULT_COLORS.text,
|
|
64
|
-
fontSize: AXIS_LABEL_FONT_SIZES.cartesian,
|
|
65
|
-
fontFamily: DEFAULT_FONT_FAMILY,
|
|
66
|
-
fontWeight: "normal",
|
|
67
|
-
rotation: 0,
|
|
68
|
-
overlap: "error"
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
function normalizeLabelLayout(config, args, operation) {
|
|
72
|
-
const next = { ...config };
|
|
73
|
-
next.rotation = Object.hasOwn(args, "rotation")
|
|
74
|
-
? resolveRotation(args.rotation, `${operation} rotation`)
|
|
75
|
-
: next.rotation ?? 0;
|
|
76
|
-
next.overlap ??= "error";
|
|
77
|
-
if (args.maxWidth === false) {
|
|
78
|
-
if (Object.hasOwn(args, "wrap") || Object.hasOwn(args, "lineHeight")) {
|
|
79
|
-
throw new Error(`${operation} cannot combine maxWidth false with wrap or lineHeight.`);
|
|
80
|
-
}
|
|
81
|
-
delete next.maxWidth;
|
|
82
|
-
delete next.wrap;
|
|
83
|
-
delete next.lineHeight;
|
|
84
|
-
} else if (next.maxWidth !== undefined) {
|
|
85
|
-
next.wrap ??= "word";
|
|
86
|
-
}
|
|
87
|
-
return next;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export { validateTextStyle as validateAxisTextStyle };
|
|
91
|
-
|
|
92
|
-
function validateOptions(args, operation, create) {
|
|
93
|
-
validateTickRounding(args.rounding, !Object.hasOwn(args, "rounding"));
|
|
94
|
-
if (Object.hasOwn(args, "rounding") && Object.hasOwn(args, "values")) {
|
|
95
|
-
throw new Error(`${operation} cannot use rounding and values together.`);
|
|
96
|
-
}
|
|
97
|
-
validateAxisTextPlacement(args);
|
|
98
|
-
validateOptionObject(
|
|
99
|
-
args,
|
|
100
|
-
create ? OPTIONS : OPTIONS.filter(key => key !== "scale"),
|
|
101
|
-
operation
|
|
102
|
-
);
|
|
103
|
-
if (Object.hasOwn(args, "flush") && typeof args.flush !== "boolean") throw new TypeError("Label flush must be a boolean.");
|
|
104
|
-
if (Object.hasOwn(args, "alongOffset") && !Number.isFinite(args.alongOffset)) throw new TypeError("Label alongOffset must be a finite number.");
|
|
105
|
-
if (Object.hasOwn(args, "count") && Object.hasOwn(args, "values")) {
|
|
106
|
-
throw new Error(`${operation} cannot use count and values together.`);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function validateConfig(channel, config) {
|
|
111
|
-
validateAxisTextPlacement(config);
|
|
112
|
-
validateAxisPosition(channel, config.position);
|
|
113
|
-
if (config.mode === "count") {
|
|
114
|
-
if (!Number.isInteger(config.count) || config.count <= 0) throw new RangeError("Label count must be a positive integer.");
|
|
115
|
-
validateGeneratedItemLimit(config.count, "Label count");
|
|
116
|
-
}
|
|
117
|
-
if (
|
|
118
|
-
config.mode === "values" &&
|
|
119
|
-
(!Array.isArray(config.values) || !config.values.every(value =>
|
|
120
|
-
typeof value === "string" ||
|
|
121
|
-
typeof value === "boolean" ||
|
|
122
|
-
(typeof value === "number" && Number.isFinite(value))
|
|
123
|
-
))
|
|
124
|
-
) throw new TypeError("Label values must be nominal values or finite numbers.");
|
|
125
|
-
if (config.mode === "values") {
|
|
126
|
-
validateGeneratedItemLimit(config.values.length, "Label value count");
|
|
127
|
-
}
|
|
128
|
-
if (config.alongOffset !== undefined && !Number.isFinite(config.alongOffset)) throw new TypeError("Label alongOffset must be a finite number.");
|
|
129
|
-
validateNonNegativeFinite(config.offset, "Label offset");
|
|
130
|
-
validateTextStyle(config, "Label");
|
|
131
|
-
validateAxisFormat(config.format);
|
|
132
|
-
if (config.flush === true && config.rotation !== 0) throw new Error("Label flush requires unrotated Cartesian labels.");
|
|
133
|
-
if (!Number.isFinite(config.rotation)) {
|
|
134
|
-
throw new TypeError("Label rotation must resolve to finite radians.");
|
|
135
|
-
}
|
|
136
|
-
if (config.maxWidth !== undefined) {
|
|
137
|
-
validatePositiveFinite(config.maxWidth, "Label maxWidth");
|
|
138
|
-
if (!["word", "character"].includes(config.wrap)) {
|
|
139
|
-
throw new Error(`Unsupported axis label wrap "${config.wrap}".`);
|
|
140
|
-
}
|
|
141
|
-
} else if (config.wrap !== undefined || config.lineHeight !== undefined) {
|
|
142
|
-
throw new Error("Axis label wrap and lineHeight require maxWidth.");
|
|
143
|
-
}
|
|
144
|
-
if (config.lineHeight !== undefined && (
|
|
145
|
-
!Number.isFinite(config.lineHeight) || config.lineHeight < config.fontSize
|
|
146
|
-
)) {
|
|
147
|
-
throw new RangeError("Axis label lineHeight must cover fontSize.");
|
|
148
|
-
}
|
|
149
|
-
if (!["error", "allow"].includes(config.overlap)) {
|
|
150
|
-
throw new Error(`Unsupported axis label overlap policy "${config.overlap}".`);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function normalizeAxisLabelMaps(program, config, args, operation) {
|
|
155
|
-
const next = { ...config };
|
|
156
|
-
for (const [property, normalize] of [
|
|
157
|
-
["labelMap", normalizeDisplayLabelMap],
|
|
158
|
-
["colorMap", normalizeDisplayColorMap]
|
|
159
|
-
]) {
|
|
160
|
-
if (!Object.hasOwn(args, property)) continue;
|
|
161
|
-
if (args[property] === "auto") delete next[property];
|
|
162
|
-
else next[property] = normalize(args[property], `${operation} ${property}`);
|
|
163
|
-
}
|
|
164
|
-
validateAxisLabelMapScale(next, program.resolvedScales[config.scale], operation);
|
|
165
|
-
return next;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function validateAxisLabelMapScale(config, scale, operation = "Axis") {
|
|
169
|
-
if (["ordinal", "band", "point"].includes(scale?.type)) return;
|
|
170
|
-
for (const property of ["labelMap", "colorMap"]) {
|
|
171
|
-
if (config[property] !== undefined &&
|
|
172
|
-
!config[property].every(entry => Number.isFinite(entry.value))) {
|
|
173
|
-
throw new TypeError(`${operation} continuous ${property} values must be finite numbers or timestamps.`);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function assertTickCompatibility(ticks, config, operation) {
|
|
179
|
-
if (!ticks) return;
|
|
180
|
-
if (ticks.scale !== config.scale || ticks.mode !== config.mode) throw new Error(`${operation} conflicts with axis ticks.`);
|
|
181
|
-
if (config.mode === "count" && ticks.count !== config.count) throw new Error(`${operation} conflicts with axis ticks.`);
|
|
182
|
-
if (config.mode === "count" && (ticks.rounding ?? "ceiling") !== (config.rounding ?? "ceiling")) {
|
|
183
|
-
throw new Error(`${operation} conflicts with axis tick rounding.`);
|
|
184
|
-
}
|
|
185
|
-
if (config.mode === "values" && !sameOrderedValues(ticks.values, config.values)) throw new Error(`${operation} conflicts with axis ticks.`);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function expandWrappedLabels(resolved, text, config, channel, profile) {
|
|
189
|
-
if (config.maxWidth === undefined) {
|
|
190
|
-
return {
|
|
191
|
-
...resolved,
|
|
192
|
-
text,
|
|
193
|
-
groups: text.map((_, index) => index)
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
const style = {
|
|
197
|
-
fontSize: config.fontSize,
|
|
198
|
-
fontFamily: config.fontFamily,
|
|
199
|
-
fontWeight: config.fontWeight
|
|
200
|
-
};
|
|
201
|
-
const lineHeight = config.lineHeight ?? config.fontSize * 1.2;
|
|
202
|
-
const expanded = { text: [], x: [], y: [], groups: [] };
|
|
203
|
-
for (let index = 0; index < text.length; index += 1) {
|
|
204
|
-
const lines = wrapText(text[index], {
|
|
205
|
-
maxWidth: config.maxWidth,
|
|
206
|
-
mode: config.wrap,
|
|
207
|
-
style, profile
|
|
208
|
-
});
|
|
209
|
-
validateGeneratedItemLimit(
|
|
210
|
-
expanded.text.length + lines.length,
|
|
211
|
-
"Axis label line count"
|
|
212
|
-
);
|
|
213
|
-
const baseX = Array.isArray(resolved.x) ? resolved.x[index] : resolved.x;
|
|
214
|
-
const baseY = Array.isArray(resolved.y) ? resolved.y[index] : resolved.y;
|
|
215
|
-
for (let line = 0; line < lines.length; line += 1) {
|
|
216
|
-
expanded.text.push(lines[line]);
|
|
217
|
-
expanded.x.push(baseX);
|
|
218
|
-
expanded.y.push(channel === "x"
|
|
219
|
-
? baseY + (resolved.textBaseline === "top" ? line : -line) * lineHeight
|
|
220
|
-
: baseY + (line - (lines.length - 1) / 2) * lineHeight);
|
|
221
|
-
expanded.groups.push(index);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
return { ...resolved, ...expanded };
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function unionLabelBounds(records) {
|
|
228
|
-
const byGroup = new Map();
|
|
229
|
-
for (const { bounds, group } of records) {
|
|
230
|
-
const previous = byGroup.get(group);
|
|
231
|
-
byGroup.set(group, previous === undefined ? bounds : {
|
|
232
|
-
left: Math.min(previous.left, bounds.left),
|
|
233
|
-
right: Math.max(previous.right, bounds.right),
|
|
234
|
-
top: Math.min(previous.top, bounds.top),
|
|
235
|
-
bottom: Math.max(previous.bottom, bounds.bottom)
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
return [...byGroup.values()];
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function resolve(program, channel, config, axisId = channel) {
|
|
242
|
-
const scale = program.resolvedScales[config.scale];
|
|
243
|
-
const bounds = resolveCartesianAxisFrameBounds(program, channel, config.position, config.scale, axisId);
|
|
244
|
-
const discrete = ["ordinal", "band", "point"].includes(scale?.type);
|
|
245
|
-
validateAxisLabelMapScale(config, scale);
|
|
246
|
-
if ((
|
|
247
|
-
!["linear", "time", "ordinal", "band", "point"].includes(scale?.type) &&
|
|
248
|
-
!isTransformedScaleType(scale?.type)
|
|
249
|
-
) || !bounds) throw new Error("Axis labels require a supported resolved scale and Canvas bounds.");
|
|
250
|
-
if (discrete && config.mode !== "values") throw new Error("Discrete axis labels require explicit or inferred values, not count.");
|
|
251
|
-
let values = valuesFromTickConfig(program, config);
|
|
252
|
-
validateGeneratedItemLimit(values.length, "Label value count");
|
|
253
|
-
if (discrete) {
|
|
254
|
-
const domainValues = new Set(scale.domain);
|
|
255
|
-
if (!values.every(value => domainValues.has(value))) throw new RangeError("Label values must be inside the scale domain.");
|
|
256
|
-
} else {
|
|
257
|
-
const low = Math.min(...scale.domain), high = Math.max(...scale.domain);
|
|
258
|
-
if (!values.every(value => value >= low && value <= high)) throw new RangeError("Label values must be inside the scale domain.");
|
|
259
|
-
}
|
|
260
|
-
let positions = discrete
|
|
261
|
-
? mapOrdinalPositionValues(values, scale)
|
|
262
|
-
: mapContinuousScaleValues(values, scale);
|
|
263
|
-
const fallback = value => formatAxisValue(
|
|
264
|
-
value,
|
|
265
|
-
scale.type,
|
|
266
|
-
config.format,
|
|
267
|
-
item => scale.type === "time"
|
|
268
|
-
? formatTimeTick(item, scale.domain)
|
|
269
|
-
: isTransformedScaleType(scale.type)
|
|
270
|
-
? formatTransformedTick(scale.type, item)
|
|
271
|
-
: String(item)
|
|
272
|
-
);
|
|
273
|
-
const automaticLabels = config.format === "ticks"
|
|
274
|
-
? formatNumericAxisTicks(values, scale.type)
|
|
275
|
-
: scale.type === "time" && config.format === "auto"
|
|
276
|
-
? formatTimeTicks(values, scale.domain)
|
|
277
|
-
: undefined;
|
|
278
|
-
let text = values.map((value, index) =>
|
|
279
|
-
resolveDisplayLabel(value, config.labelMap, raw =>
|
|
280
|
-
automaticLabels?.[index] ?? fallback(raw)));
|
|
281
|
-
// Empty mapped text suppresses the label, never its raw tick or scale value.
|
|
282
|
-
// Filter all aligned arrays before wrapping so group/color indices stay dense.
|
|
283
|
-
if (text.includes("")) {
|
|
284
|
-
const visible = text.map((value, index) => value === "" ? -1 : index)
|
|
285
|
-
.filter(index => index !== -1);
|
|
286
|
-
values = visible.map(index => values[index]);
|
|
287
|
-
positions = visible.map(index => positions[index]);
|
|
288
|
-
text = visible.map(index => text[index]);
|
|
289
|
-
}
|
|
290
|
-
const geometry = {
|
|
291
|
-
values,
|
|
292
|
-
...resolveAxisLabelGeometry({
|
|
293
|
-
bounds,
|
|
294
|
-
channel,
|
|
295
|
-
position: config.position,
|
|
296
|
-
positions: positions.map(value => value + (config.alongOffset ?? 0)),
|
|
297
|
-
offset: config.offset
|
|
298
|
-
})
|
|
299
|
-
};
|
|
300
|
-
const resolved = expandWrappedLabels(geometry, text, config, channel, program.materializationConfigs.textMetrics);
|
|
301
|
-
if (channel === "x" && Math.abs(Math.sin(config.rotation)) > 1e-9) {
|
|
302
|
-
const outward = config.position === "bottom" ? 1 : -1;
|
|
303
|
-
resolved.textAlign = Math.sin(config.rotation) * outward < 0 ? "right" : "left";
|
|
304
|
-
resolved.textBaseline = "middle";
|
|
305
|
-
}
|
|
306
|
-
const canvas = findCanvasGraphic(program)?.properties;
|
|
307
|
-
const measuredBounds = () => resolved.text.map((value, index) => resolveTextBounds({
|
|
308
|
-
x: Array.isArray(resolved.x) ? resolved.x[index] : resolved.x,
|
|
309
|
-
y: Array.isArray(resolved.y) ? resolved.y[index] : resolved.y,
|
|
310
|
-
text: value,
|
|
311
|
-
fontSize: config.fontSize,
|
|
312
|
-
fontFamily: config.fontFamily,
|
|
313
|
-
fontWeight: config.fontWeight,
|
|
314
|
-
textAlign: resolved.textAlign,
|
|
315
|
-
textBaseline: resolved.textBaseline,
|
|
316
|
-
rotation: config.rotation
|
|
317
|
-
}, program.materializationConfigs.textMetrics));
|
|
318
|
-
let labelBounds = measuredBounds();
|
|
319
|
-
if (program.materializationConfigs.canvas?.plot !== undefined || config.rotation !== 0) {
|
|
320
|
-
const horizontal = channel === "x";
|
|
321
|
-
const near = ["bottom", "right"].includes(config.position);
|
|
322
|
-
const edge = horizontal ? bounds.y + (near ? bounds.height : 0)
|
|
323
|
-
: bounds.x + (near ? bounds.width : 0);
|
|
324
|
-
const boundKey = horizontal ? (near ? "top" : "bottom") : (near ? "left" : "right");
|
|
325
|
-
const grouped = unionLabelBounds(labelBounds.map((bounds, index) => ({ bounds, group: resolved.groups[index] })));
|
|
326
|
-
const shifts = grouped.map(item => edge + (near ? config.offset : -config.offset) - item[boundKey]);
|
|
327
|
-
const axis = horizontal ? "y" : "x";
|
|
328
|
-
const positions = resolved[axis];
|
|
329
|
-
resolved[axis] = resolved.text.map((_, index) =>
|
|
330
|
-
(Array.isArray(positions) ? positions[index] : positions) + shifts[resolved.groups[index]]);
|
|
331
|
-
labelBounds = labelBounds.map((item, index) => {
|
|
332
|
-
const delta = shifts[resolved.groups[index]];
|
|
333
|
-
return horizontal ? { ...item, top: item.top + delta, bottom: item.bottom + delta }
|
|
334
|
-
: { ...item, left: item.left + delta, right: item.right + delta };
|
|
335
|
-
});
|
|
336
|
-
}
|
|
337
|
-
if (config.flush === true) {
|
|
338
|
-
const along = channel === "x" ? "x" : "y";
|
|
339
|
-
const lowKey = channel === "x" ? "left" : "top";
|
|
340
|
-
const highKey = channel === "x" ? "right" : "bottom";
|
|
341
|
-
const low = Math.min(...scale.range) + (config.alongOffset ?? 0);
|
|
342
|
-
const high = Math.max(...scale.range) + (config.alongOffset ?? 0);
|
|
343
|
-
const grouped = unionLabelBounds(labelBounds.map((bounds, index) => ({ bounds, group: resolved.groups[index] })));
|
|
344
|
-
const shifts = grouped.map(item => {
|
|
345
|
-
if (item[highKey] - item[lowKey] > high - low + 1e-9) {
|
|
346
|
-
throw new Error(`The ${channel}-axis label cannot flush inside the scale span.`);
|
|
347
|
-
}
|
|
348
|
-
return item[lowKey] < low ? low - item[lowKey]
|
|
349
|
-
: item[highKey] > high ? high - item[highKey] : 0;
|
|
350
|
-
});
|
|
351
|
-
const positions = resolved[along];
|
|
352
|
-
resolved[along] = resolved.text.map((_, index) =>
|
|
353
|
-
(Array.isArray(positions) ? positions[index] : positions) + shifts[resolved.groups[index]]);
|
|
354
|
-
labelBounds = labelBounds.map((item, index) => {
|
|
355
|
-
const delta = shifts[resolved.groups[index]];
|
|
356
|
-
return { ...item, [lowKey]: item[lowKey] + delta, [highKey]: item[highKey] + delta };
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
if (config.align !== undefined) {
|
|
360
|
-
resolved.textAlign = config.align;
|
|
361
|
-
labelBounds = measuredBounds();
|
|
362
|
-
}
|
|
363
|
-
const rotatedOverlap = config.rotation !== 0;
|
|
364
|
-
const collisionBounds = rotatedOverlap ? resolved.text.map((text, index) => {
|
|
365
|
-
const x = Array.isArray(resolved.x) ? resolved.x[index] : resolved.x;
|
|
366
|
-
const y = Array.isArray(resolved.y) ? resolved.y[index] : resolved.y;
|
|
367
|
-
const cosine = Math.cos(config.rotation), sine = Math.sin(config.rotation);
|
|
368
|
-
return resolveTextBounds({
|
|
369
|
-
x: x * cosine + y * sine, y: -x * sine + y * cosine, text,
|
|
370
|
-
fontSize: config.fontSize, fontFamily: config.fontFamily, fontWeight: config.fontWeight,
|
|
371
|
-
textAlign: resolved.textAlign, textBaseline: resolved.textBaseline
|
|
372
|
-
}, program.materializationConfigs.textMetrics);
|
|
373
|
-
}) : labelBounds;
|
|
374
|
-
const groupedBounds = unionLabelBounds(collisionBounds.map((bounds, index) => ({
|
|
375
|
-
bounds,
|
|
376
|
-
group: resolved.groups[index]
|
|
377
|
-
})));
|
|
378
|
-
const orderedBounds = groupedBounds.sort((left, right) => channel === "x"
|
|
379
|
-
? left.left - right.left
|
|
380
|
-
: left.top - right.top);
|
|
381
|
-
if (!canvas || !labelBounds.every(item => textBoundsFitCanvas(item, canvas))) {
|
|
382
|
-
throw canvasOverflowError(`The ${channel}-axis labels do not fit the Canvas margin.`, labelBounds, canvas);
|
|
383
|
-
}
|
|
384
|
-
if (config.overlap === "error" && orderedBounds.some((item, index) => rotatedOverlap
|
|
385
|
-
? orderedBounds.slice(0, index).some(previous => textBoundsIntersect(previous, item))
|
|
386
|
-
: index > 0 && textBoundsIntersect(orderedBounds[index - 1], item))) {
|
|
387
|
-
throw new Error(`The ${channel}-axis labels overlap each other.`);
|
|
388
|
-
}
|
|
389
|
-
const title = program.graphicSpec.objects[axisGraphicIds(channel, axisId)[3]]
|
|
390
|
-
? resolveConcreteGraphicBounds(program.graphicSpec, axisGraphicIds(channel, axisId)[3], program.materializationConfigs.textMetrics)
|
|
391
|
-
: undefined;
|
|
392
|
-
if (title && !axisTitleAllowsLabelOverlap(program.guideConfigs.axis?.[axisId]?.title) &&
|
|
393
|
-
program.guideConfigs.axis?.[axisId]?.title?.inferredOffset !== true &&
|
|
394
|
-
labelBounds.some(item => textBoundsIntersect(item, title))) {
|
|
395
|
-
throw new Error(`The ${channel}-axis labels overlap the axis title.`);
|
|
396
|
-
}
|
|
397
|
-
return resolved;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
function makeEdit(channel) {
|
|
401
|
-
const op = channel === "x" ? "editXAxisLabels" : "editYAxisLabels";
|
|
402
|
-
return action({ op, description: `Edit concrete ${channel}-axis labels.` }, withGuideLayoutValidation(function (args = {}) {
|
|
403
|
-
validateOptions(args, op, false);
|
|
404
|
-
const selected = selectCartesianAxis(this, channel, args, { create: false });
|
|
405
|
-
const axisId = selected.id; args = selected.options;
|
|
406
|
-
const id = axisGraphicIds(channel, axisId)[2];
|
|
407
|
-
if (this.graphicSpec.objects[id]?.type !== "text") throw new Error(`${op} requires existing axis labels.`);
|
|
408
|
-
const previous = this.guideConfigs.axis?.[axisId]?.labels;
|
|
409
|
-
if (!previous) throw new Error(`${op} requires label configuration.`);
|
|
410
|
-
const explicitMode = Object.hasOwn(args, "values") || Object.hasOwn(args, "count");
|
|
411
|
-
const ticks = this.guideConfigs.axis?.[axisId]?.ticks;
|
|
412
|
-
const inferredValues = !explicitMode && previous.inferredValues === true &&
|
|
413
|
-
ticks?.mode === "values"
|
|
414
|
-
? ticks.values
|
|
415
|
-
: undefined;
|
|
416
|
-
const mode = Object.hasOwn(args, "values") || inferredValues !== undefined
|
|
417
|
-
? "values"
|
|
418
|
-
: Object.hasOwn(args, "count") ? "count" : previous.mode;
|
|
419
|
-
let config = normalizeLabelLayout({
|
|
420
|
-
...previous,
|
|
421
|
-
...args,
|
|
422
|
-
...(inferredValues === undefined ? {} : { values: inferredValues }),
|
|
423
|
-
...(explicitMode ? { inferredValues: false } : {}),
|
|
424
|
-
mode
|
|
425
|
-
}, args, op);
|
|
426
|
-
config = normalizeAxisLabelMaps(this, config, args, op);
|
|
427
|
-
if (mode === "values") {
|
|
428
|
-
delete config.count;
|
|
429
|
-
if (!Object.hasOwn(args, "rounding")) delete config.rounding;
|
|
430
|
-
} else delete config.values;
|
|
431
|
-
validateConfig(channel, config);
|
|
432
|
-
assertTickCompatibility(this.guideConfigs.axis?.[axisId]?.ticks, config, op);
|
|
433
|
-
const resolved = resolve(this, channel, config, axisId);
|
|
434
|
-
let next = this._withGuideConfig(axisId, "labels", config);
|
|
435
|
-
const properties = {
|
|
436
|
-
length: resolved.text.length,
|
|
437
|
-
x: resolved.x,
|
|
438
|
-
y: resolved.y,
|
|
439
|
-
text: resolved.text,
|
|
440
|
-
fill: config.colorMap === undefined ? config.color
|
|
441
|
-
: resolved.text.map((_, index) => resolveDisplayLabelColor(
|
|
442
|
-
resolved.values[resolved.groups[index]], config.colorMap, config.color)),
|
|
443
|
-
fontSize: config.fontSize,
|
|
444
|
-
fontFamily: config.fontFamily,
|
|
445
|
-
fontWeight: config.fontWeight,
|
|
446
|
-
textAlign: resolved.textAlign,
|
|
447
|
-
textBaseline: resolved.textBaseline
|
|
448
|
-
};
|
|
449
|
-
if (config.rotation !== 0 || this.graphicSpec.objects[id].items.some(
|
|
450
|
-
item => Object.hasOwn(item.properties, "rotation")
|
|
451
|
-
)) properties.rotation = config.rotation;
|
|
452
|
-
for (const [property, value] of Object.entries(properties)) {
|
|
453
|
-
next = next.editGraphics({ target: id, property, value });
|
|
454
|
-
}
|
|
455
|
-
const titleConfig = next.guideConfigs.axis?.[axisId]?.title;
|
|
456
|
-
if (titleConfig?.inferredOffset === true) {
|
|
457
|
-
const editTitle = channel === "x" ? "editXAxisTitle" : "editYAxisTitle";
|
|
458
|
-
next = next[editTitle](axisId === channel ? undefined : axisTargetArgs(axisId, channel));
|
|
459
|
-
}
|
|
460
|
-
return next;
|
|
461
|
-
}));
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
const editXAxisLabels = /* @__PURE__ */ makeEdit("x");
|
|
465
|
-
const editYAxisLabels = /* @__PURE__ */ makeEdit("y");
|
|
466
|
-
|
|
467
|
-
function makeCreate(channel) {
|
|
468
|
-
const op = channel === "x" ? "createXAxisLabels" : "createYAxisLabels";
|
|
469
|
-
const edit = channel === "x" ? "editXAxisLabels" : "editYAxisLabels";
|
|
470
|
-
return action({ op, description: `Create concrete ${channel}-axis labels.` }, withGuideLayoutValidation(function (args = {}) {
|
|
471
|
-
validateOptions(args, op, true);
|
|
472
|
-
const selected = selectCartesianAxis(this, channel, args, { create: true });
|
|
473
|
-
const axisId = selected.id; args = selected.options;
|
|
474
|
-
const scale = validateUserId(args.scale ?? channel, "Scale id");
|
|
475
|
-
const guideScale = this.semanticSpec.guides.axis?.[axisId]?.scale;
|
|
476
|
-
if (guideScale && guideScale !== scale) throw new Error(`${op} conflicts with the existing axis scale.`);
|
|
477
|
-
const id = axisGraphicIds(channel, axisId)[2];
|
|
478
|
-
if (this.graphicSpec.objects[id]) throw new Error(`${op} requires missing axis labels.`);
|
|
479
|
-
const ticks = this.guideConfigs.axis?.[axisId]?.ticks;
|
|
480
|
-
const hasValues = Object.hasOwn(args, "values");
|
|
481
|
-
const hasCount = Object.hasOwn(args, "count");
|
|
482
|
-
const resolvedScale = this.resolvedScales[scale];
|
|
483
|
-
const inferredDiscreteValues = !hasValues && !hasCount && ticks === undefined &&
|
|
484
|
-
["ordinal", "band", "point"].includes(resolvedScale?.type)
|
|
485
|
-
? resolvedScale.domain
|
|
486
|
-
: undefined;
|
|
487
|
-
const mode = hasValues || inferredDiscreteValues !== undefined
|
|
488
|
-
? "values"
|
|
489
|
-
: hasCount ? "count" : ticks?.mode ?? "count";
|
|
490
|
-
let config = normalizeLabelLayout({
|
|
491
|
-
scale,
|
|
492
|
-
position: defaultAxisPosition(channel),
|
|
493
|
-
offset: channel === "x" ? 18 : 12,
|
|
494
|
-
format: "auto",
|
|
495
|
-
color: DEFAULTS.color,
|
|
496
|
-
fontSize: DEFAULTS.fontSize,
|
|
497
|
-
fontFamily: DEFAULTS.fontFamily,
|
|
498
|
-
fontWeight: DEFAULTS.fontWeight,
|
|
499
|
-
...axisThemeTypography(this.materializationConfigs.theme, "labels"),
|
|
500
|
-
rotation: DEFAULTS.rotation,
|
|
501
|
-
overlap: DEFAULTS.overlap,
|
|
502
|
-
...(ticks?.rounding === undefined ? {} : { rounding: ticks.rounding }),
|
|
503
|
-
...args,
|
|
504
|
-
inferredValues: !hasValues && !hasCount &&
|
|
505
|
-
(ticks?.inferredValues === true || inferredDiscreteValues !== undefined),
|
|
506
|
-
mode
|
|
507
|
-
}, args, op);
|
|
508
|
-
config = normalizeAxisLabelMaps(this, config, args, op);
|
|
509
|
-
if (mode === "values") {
|
|
510
|
-
config.values ??= ticks?.values ?? inferredDiscreteValues;
|
|
511
|
-
if (!Object.hasOwn(args, "rounding")) delete config.rounding;
|
|
512
|
-
} else {
|
|
513
|
-
config.count ??= ticks?.count ?? DEFAULTS.count;
|
|
514
|
-
}
|
|
515
|
-
validateConfig(channel, config);
|
|
516
|
-
assertTickCompatibility(ticks, config, op);
|
|
517
|
-
resolve(this, channel, config, axisId);
|
|
518
|
-
return this.editSemantic({ property: `guide.axis.${axisId}.scale`, value: scale })
|
|
519
|
-
.createGraphics({
|
|
520
|
-
id,
|
|
521
|
-
type: "text",
|
|
522
|
-
length: 0,
|
|
523
|
-
...resolvePlotGraphicPlacement(this)
|
|
524
|
-
})
|
|
525
|
-
._withGuideConfig(axisId, "labels", config)[edit](axisId === channel ? undefined : axisTargetArgs(axisId, channel));
|
|
526
|
-
}));
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
const createXAxisLabels = /* @__PURE__ */ makeCreate("x");
|
|
530
|
-
const createYAxisLabels = /* @__PURE__ */ makeCreate("y");
|
|
531
|
-
|
|
532
|
-
export function registerAxisLabelActions(Class) {
|
|
533
|
-
Class.prototype.editXAxisLabels = editXAxisLabels;
|
|
534
|
-
Class.prototype.editYAxisLabels = editYAxisLabels;
|
|
535
|
-
Class.prototype.createXAxisLabels = createXAxisLabels;
|
|
536
|
-
Class.prototype.createYAxisLabels = createYAxisLabels;
|
|
537
|
-
}
|
|
1
|
+
import{axisTitleAllowsLabelOverlap as ie,axisTitleFollowsLabels as N}from"../../../grammar/axisTextPlacement.js";import{selectCartesianAxis as V,axisTargetArgs as G}from"../../../selectors/axes.js";import{axisGraphicIds as C,hasPendingAxisTitleRematerialization as se}from"../../../materialization/guides/resources.js";import{axisThemeTypography as re}from"../../theme/state.js";import{validateAxisLabelFlushThreshold as H,validateAxisTextPlacement as _}from"../../../grammar/axisTextPlacement.js";import{canvasOverflowError as ae}from"../../../layout/canvas.js";import{withGuideLayoutValidation as X}from"../../../materialization/guides/layout.js";import{action as Y}from"../../../core/action.js";import{validateUserId as ne}from"../../../core/identifiers.js";import{sameOrderedValues as le,validateGeneratedItemLimit as F,validateTextStyle as P,validateOptionObject as ue,validateNonNegativeFinite as de,validatePositiveFinite as fe}from"../../../core/validation.js";import{resolveCartesianAxisFrameBounds as pe}from"../../../materialization/coordinateBounds.js";import{isTransformedScaleType as R,mapContinuousScaleValues as he,formatTransformedTick as me,mapOrdinalPositionValues as ce}from"../../../grammar/scales/index.js";import{formatTimeTick as xe,formatTimeTicks as be,validateTickRounding as we}from"../../../grammar/ticks.js";import{resolveRotation as ye}from"../../../grammar/rotation.js";import{valuesFromTickConfig as ve}from"../tickValues.js";import{AXIS_LABEL_FONT_SIZES as Ae,DEFAULT_COLORS as Oe,DEFAULT_FONT_FAMILY as Le}from"../../../theme/defaults.js";import{defaultAxisPosition as Te,formatAxisValue as Se,formatNumericAxisTicks as Ee,resolveAxisLabelGeometry as ge,validateAxisFormat as Me,validateAxisPosition as Ce}from"./policy.js";import{findCanvasGraphic as Fe,resolvePlotGraphicPlacement as ze}from"../../../materialization/graphicHierarchy.js";import{resolveTextBounds as D,textBoundsFitCanvas as je,textBoundsIntersect as j}from"../../../core/textMetrics.js";import{resolveConcreteGraphicBounds as We}from"../../../grammar/schemas/graphicBounds.js";import{wrapText as $e}from"../../../layout/text.js";import{normalizeDisplayLabelMap as Be,normalizeDisplayColorMap as ke,resolveDisplayLabelColor as Ne,resolveDisplayLabel as Ve}from"../../../grammar/displayLabels.js";const q=["target","scale","position","count","values","rounding","offset","format","color","fontSize","fontFamily","fontWeight","rotation","maxWidth","wrap","lineHeight","overlap","labelMap","alongOffset","flush","flushThreshold","colorMap","align"],S={count:5,color:Oe.text,fontSize:Ae.cartesian,fontFamily:Le,fontWeight:"normal",rotation:0,overlap:"error"};function U(o,e,t){const s={...o};if(s.rotation=Object.hasOwn(e,"rotation")?ye(e.rotation,`${t} rotation`):s.rotation??0,s.overlap??="error",e.maxWidth===!1){if(Object.hasOwn(e,"wrap")||Object.hasOwn(e,"lineHeight"))throw new Error(`${t} cannot combine maxWidth false with wrap or lineHeight.`);delete s.maxWidth,delete s.wrap,delete s.lineHeight}else s.maxWidth!==void 0&&(s.wrap??="word");return s}function W(o,e,t){if(we(o.rounding,!Object.hasOwn(o,"rounding")),Object.hasOwn(o,"rounding")&&Object.hasOwn(o,"values"))throw new Error(`${e} cannot use rounding and values together.`);if(_(o),H(o.flushThreshold,{allowReset:!0}),ue(o,t?q:q.filter(s=>s!=="scale"),e),Object.hasOwn(o,"flush")&&typeof o.flush!="boolean")throw new TypeError("Label flush must be a boolean.");if(Object.hasOwn(o,"alongOffset")&&!Number.isFinite(o.alongOffset))throw new TypeError("Label alongOffset must be a finite number.");if(Object.hasOwn(o,"count")&&Object.hasOwn(o,"values"))throw new Error(`${e} cannot use count and values together.`)}function I(o,e){if(_(e),Ce(o,e.position),e.mode==="count"){if(!Number.isInteger(e.count)||e.count<=0)throw new RangeError("Label count must be a positive integer.");F(e.count,"Label count")}if(e.mode==="values"&&(!Array.isArray(e.values)||!e.values.every(t=>typeof t=="string"||typeof t=="boolean"||typeof t=="number"&&Number.isFinite(t))))throw new TypeError("Label values must be nominal values or finite numbers.");if(e.mode==="values"&&F(e.values.length,"Label value count"),e.alongOffset!==void 0&&!Number.isFinite(e.alongOffset))throw new TypeError("Label alongOffset must be a finite number.");if(de(e.offset,"Label offset"),P(e,"Label"),Me(e.format),H(e.flushThreshold),e.flush===!0&&e.rotation!==0)throw new Error("Label flush requires unrotated Cartesian labels.");if(!Number.isFinite(e.rotation))throw new TypeError("Label rotation must resolve to finite radians.");if(e.maxWidth!==void 0){if(fe(e.maxWidth,"Label maxWidth"),!["word","character"].includes(e.wrap))throw new Error(`Unsupported axis label wrap "${e.wrap}".`)}else if(e.wrap!==void 0||e.lineHeight!==void 0)throw new Error("Axis label wrap and lineHeight require maxWidth.");if(e.lineHeight!==void 0&&(!Number.isFinite(e.lineHeight)||e.lineHeight<e.fontSize))throw new RangeError("Axis label lineHeight must cover fontSize.");if(!["error","allow"].includes(e.overlap))throw new Error(`Unsupported axis label overlap policy "${e.overlap}".`)}function K(o,e,t,s){const i={...e};t.flushThreshold==="auto"&&delete i.flushThreshold;for(const[u,d]of[["labelMap",Be],["colorMap",ke]])Object.hasOwn(t,u)&&(t[u]==="auto"?delete i[u]:i[u]=d(t[u],`${s} ${u}`));return Z(i,o.resolvedScales[e.scale],s),i}function Z(o,e,t="Axis"){if(!["ordinal","band","point"].includes(e?.type)){for(const s of["labelMap","colorMap"])if(o[s]!==void 0&&!o[s].every(i=>Number.isFinite(i.value)))throw new TypeError(`${t} continuous ${s} values must be finite numbers or timestamps.`)}}function J(o,e,t){if(o){if(o.scale!==e.scale||o.mode!==e.mode)throw new Error(`${t} conflicts with axis ticks.`);if(e.mode==="count"&&o.count!==e.count)throw new Error(`${t} conflicts with axis ticks.`);if(e.mode==="count"&&(o.rounding??"ceiling")!==(e.rounding??"ceiling"))throw new Error(`${t} conflicts with axis tick rounding.`);if(e.mode==="values"&&!le(o.values,e.values))throw new Error(`${t} conflicts with axis ticks.`)}}function Ge(o,e,t,s,i){if(t.maxWidth===void 0)return{...o,text:e,groups:e.map((f,h)=>h)};const u={fontSize:t.fontSize,fontFamily:t.fontFamily,fontWeight:t.fontWeight},d=t.lineHeight??t.fontSize*1.2,l={text:[],x:[],y:[],groups:[]};for(let f=0;f<e.length;f+=1){const h=$e(e[f],{maxWidth:t.maxWidth,mode:t.wrap,style:u,profile:i});F(l.text.length+h.length,"Axis label line count");const x=Array.isArray(o.x)?o.x[f]:o.x,c=Array.isArray(o.y)?o.y[f]:o.y;for(let b=0;b<h.length;b+=1)l.text.push(h[b]),l.x.push(x),l.y.push(s==="x"?c+(o.textBaseline==="top"?b:-b)*d:c+(b-(h.length-1)/2)*d),l.groups.push(f)}return{...o,...l}}function $(o){const e=new Map;for(const{bounds:t,group:s}of o){const i=e.get(s);e.set(s,i===void 0?t:{left:Math.min(i.left,t.left),right:Math.max(i.right,t.right),top:Math.min(i.top,t.top),bottom:Math.max(i.bottom,t.bottom)})}return[...e.values()]}function Q(o,e,t,s=e){const i=o.resolvedScales[t.scale],u=pe(o,e,t.position,t.scale,s),d=["ordinal","band","point"].includes(i?.type);if(Z(t,i),!["linear","time","ordinal","band","point"].includes(i?.type)&&!R(i?.type)||!u)throw new Error("Axis labels require a supported resolved scale and Canvas bounds.");if(d&&t.mode!=="values")throw new Error("Discrete axis labels require explicit or inferred values, not count.");let l=ve(o,t);if(F(l.length,"Label value count"),d){const n=new Set(i.domain);if(!l.every(r=>n.has(r)))throw new RangeError("Label values must be inside the scale domain.")}else{const n=Math.min(...i.domain),r=Math.max(...i.domain);if(!l.every(m=>m>=n&&m<=r))throw new RangeError("Label values must be inside the scale domain.")}let f=d?ce(l,i):he(l,i);const h=n=>Se(n,i.type,t.format,r=>i.type==="time"?xe(r,i.domain):R(i.type)?me(i.type,r):String(r)),x=t.format==="ticks"?Ee(l,i.type):i.type==="time"&&t.format==="auto"?be(l,i.domain):void 0;let c=l.map((n,r)=>Ve(n,t.labelMap,m=>x?.[r]??h(m)));if(c.includes("")){const n=c.map((r,m)=>r===""?-1:m).filter(r=>r!==-1);l=n.map(r=>l[r]),f=n.map(r=>f[r]),c=n.map(r=>c[r])}const b={values:l,...ge({bounds:u,channel:e,position:t.position,positions:f.map(n=>n+(t.alongOffset??0)),offset:t.offset})},a=Ge(b,c,t,e,o.materializationConfigs.textMetrics);if(e==="x"&&Math.abs(Math.sin(t.rotation))>1e-9){const n=t.position==="bottom"?1:-1;a.textAlign=Math.sin(t.rotation)*n<0?"right":"left",a.textBaseline="middle"}const E=Fe(o)?.properties,y=()=>a.text.map((n,r)=>D({x:Array.isArray(a.x)?a.x[r]:a.x,y:Array.isArray(a.y)?a.y[r]:a.y,text:n,fontSize:t.fontSize,fontFamily:t.fontFamily,fontWeight:t.fontWeight,textAlign:a.textAlign,textBaseline:a.textBaseline,rotation:t.rotation},o.materializationConfigs.textMetrics));let v=y();if(o.materializationConfigs.canvas?.plot!==void 0||t.rotation!==0){const n=e==="x",r=["bottom","right"].includes(t.position),m=n?u.y+(r?u.height:0):u.x+(r?u.width:0),O=n?r?"top":"bottom":r?"left":"right",g=$(v.map((p,w)=>({bounds:p,group:a.groups[w]}))).map(p=>m+(r?t.offset:-t.offset)-p[O]),M=n?"y":"x",T=a[M];a[M]=a.text.map((p,w)=>(Array.isArray(T)?T[w]:T)+g[a.groups[w]]),v=v.map((p,w)=>{const A=g[a.groups[w]];return n?{...p,top:p.top+A,bottom:p.bottom+A}:{...p,left:p.left+A,right:p.right+A}})}if(t.flush===!0){const n=e==="x"?"x":"y",r=e==="x"?"left":"top",m=e==="x"?"right":"bottom",O=Math.min(...i.range)+(t.alongOffset??0),L=Math.max(...i.range)+(t.alongOffset??0),M=$(v.map((p,w)=>({bounds:p,group:a.groups[w]}))).map((p,w)=>{const A=f[w]+(t.alongOffset??0);if(t.flushThreshold!==void 0&&Math.min(Math.abs(A-O),Math.abs(A-L))>t.flushThreshold)return 0;if(p[m]-p[r]>L-O+1e-9)throw new Error(`The ${e}-axis label cannot flush inside the scale span.`);return p[r]<O?O-p[r]:p[m]>L?L-p[m]:0}),T=a[n];a[n]=a.text.map((p,w)=>(Array.isArray(T)?T[w]:T)+M[a.groups[w]]),v=v.map((p,w)=>{const A=M[a.groups[w]];return{...p,[r]:p[r]+A,[m]:p[m]+A}})}t.align!==void 0&&(a.textAlign=t.align,v=y());const B=t.rotation!==0,oe=B?a.text.map((n,r)=>{const m=Array.isArray(a.x)?a.x[r]:a.x,O=Array.isArray(a.y)?a.y[r]:a.y,L=Math.cos(t.rotation),g=Math.sin(t.rotation);return D({x:m*L+O*g,y:-m*g+O*L,text:n,fontSize:t.fontSize,fontFamily:t.fontFamily,fontWeight:t.fontWeight,textAlign:a.textAlign,textBaseline:a.textBaseline},o.materializationConfigs.textMetrics)}):v,z=$(oe.map((n,r)=>({bounds:n,group:a.groups[r]}))).sort((n,r)=>e==="x"?n.left-r.left:n.top-r.top);if(!E||!v.every(n=>je(n,E)))throw ae(`The ${e}-axis labels do not fit the Canvas margin.`,v,E);if(t.overlap==="error"&&z.some((n,r)=>B?z.slice(0,r).some(m=>j(m,n)):r>0&&j(z[r-1],n)))throw new Error(`The ${e}-axis labels overlap each other.`);const k=o.graphicSpec.objects[C(e,s)[3]]?We(o.graphicSpec,C(e,s)[3],o.materializationConfigs.textMetrics):void 0;if(k&&!ie(o.guideConfigs.axis?.[s]?.title)&&!N(o.guideConfigs.axis?.[s]?.title)&&!se(o,e,s)&&v.some(n=>j(n,k)))throw new Error(`The ${e}-axis labels overlap the axis title.`);return a}function He(o,e,t,s=e){const i=e==="x"?"editXAxisLabels":"editYAxisLabels";W(t,i,!1);const u=o.guideConfigs.axis?.[s]?.labels;if(!u)throw new Error(`${i} requires label configuration.`);const d=Object.hasOwn(t,"values")||Object.hasOwn(t,"count"),l=o.guideConfigs.axis?.[s]?.ticks,f=!d&&u.inferredValues===!0&&l?.mode==="values"?l.values:void 0,h=Object.hasOwn(t,"values")||f!==void 0?"values":Object.hasOwn(t,"count")?"count":u.mode;let x=U({...u,...t,...f===void 0?{}:{values:f},...d?{inferredValues:!1}:{},mode:h},t,i);return x=K(o,x,t,i),h==="values"?(delete x.count,Object.hasOwn(t,"rounding")||delete x.rounding):delete x.values,I(e,x),x}function ee(o){const e=o==="x"?"editXAxisLabels":"editYAxisLabels";return Y({op:e,description:`Edit concrete ${o}-axis labels.`},X(function(t={}){W(t,e,!1);const s=V(this,o,t,{create:!1}),i=s.id;t=s.options;const u=C(o,i)[2];if(this.graphicSpec.objects[u]?.type!=="text")throw new Error(`${e} requires existing axis labels.`);const d=He(this,o,t,i);J(this.guideConfigs.axis?.[i]?.ticks,d,e);const l=Q(this,o,d,i);let f=this._withGuideConfig(i,"labels",d);const h={length:l.text.length,x:l.x,y:l.y,text:l.text,fill:d.colorMap===void 0?d.color:l.text.map((c,b)=>Ne(l.values[l.groups[b]],d.colorMap,d.color)),fontSize:d.fontSize,fontFamily:d.fontFamily,fontWeight:d.fontWeight,textAlign:l.textAlign,textBaseline:l.textBaseline};(d.rotation!==0||this.graphicSpec.objects[u].items.some(c=>Object.hasOwn(c.properties,"rotation")))&&(h.rotation=d.rotation);for(const[c,b]of Object.entries(h))f=f.editGraphics({target:u,property:c,value:b});const x=f.guideConfigs.axis?.[i]?.title;return N(x)&&f.context.pendingRelativeTitleAxis!==i&&(f=f[o==="x"?"editXAxisTitle":"editYAxisTitle"](i===o?void 0:G(i,o))),f}))}const _e=ee("x"),Xe=ee("y");function te(o){const e=o==="x"?"createXAxisLabels":"createYAxisLabels",t=o==="x"?"editXAxisLabels":"editYAxisLabels";return Y({op:e,description:`Create concrete ${o}-axis labels.`},X(function(s={}){W(s,e,!0);const i=V(this,o,s,{create:!0}),u=i.id;s=i.options;const d=ne(s.scale??o,"Scale id"),l=this.semanticSpec.guides.axis?.[u]?.scale;if(l&&l!==d)throw new Error(`${e} conflicts with the existing axis scale.`);const f=C(o,u)[2];if(this.graphicSpec.objects[f])throw new Error(`${e} requires missing axis labels.`);const h=this.guideConfigs.axis?.[u]?.ticks,x=Object.hasOwn(s,"values"),c=Object.hasOwn(s,"count"),b=this.resolvedScales[d],a=!x&&!c&&h===void 0&&["ordinal","band","point"].includes(b?.type)?b.domain:void 0,E=x||a!==void 0?"values":c?"count":h?.mode??"count";let y=U({scale:d,position:Te(o),offset:o==="x"?18:12,format:"auto",color:S.color,fontSize:S.fontSize,fontFamily:S.fontFamily,fontWeight:S.fontWeight,...re(this.materializationConfigs.theme,"labels"),rotation:S.rotation,overlap:S.overlap,...h?.rounding===void 0?{}:{rounding:h.rounding},...s,inferredValues:!x&&!c&&(h?.inferredValues===!0||a!==void 0),mode:E},s,e);return y=K(this,y,s,e),E==="values"?(y.values??=h?.values??a,Object.hasOwn(s,"rounding")||delete y.rounding):y.count??=h?.count??S.count,I(o,y),J(h,y,e),Q(this,o,y,u),this.editSemantic({property:`guide.axis.${u}.scale`,value:d}).createGraphics({id:f,type:"text",length:0,...ze(this)})._withGuideConfig(u,"labels",y)[t](u===o?void 0:G(u,o))}))}const Ye=te("x"),Pe=te("y");function mt(o){o.prototype.editXAxisLabels=_e,o.prototype.editYAxisLabels=Xe,o.prototype.createXAxisLabels=Ye,o.prototype.createYAxisLabels=Pe}export{mt as registerAxisLabelActions,He as resolveAxisLabelEditConfig,P as validateAxisTextStyle};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { resolveAxisTickEditConfig } from "./ticks.js";
|
|
2
|
+
import { resolveAxisLabelEditConfig } from "./labels.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";
|
|
@@ -35,7 +37,7 @@ const LABEL_OPTIONS = Object.freeze([
|
|
|
35
37
|
"overlap",
|
|
36
38
|
"labelMap",
|
|
37
39
|
"alongOffset",
|
|
38
|
-
"flush",
|
|
40
|
+
"flush", "flushThreshold",
|
|
39
41
|
"colorMap",
|
|
40
42
|
"align"
|
|
41
43
|
]);
|
|
@@ -94,6 +96,19 @@ function names(channel) {
|
|
|
94
96
|
};
|
|
95
97
|
}
|
|
96
98
|
|
|
99
|
+
// Validate original options before a materialization plan stores normalized arguments.
|
|
100
|
+
export function validateAxisTickGroupEditConfig(program, channel, args, axisId = channel) {
|
|
101
|
+
validateAxisTickGroupArgs(args, names(channel).edit, false);
|
|
102
|
+
const shared = select(args, SHARED_EDIT);
|
|
103
|
+
const hasShared = Object.keys(shared).length > 0;
|
|
104
|
+
if (hasShared || Object.hasOwn(args, "ticks")) {
|
|
105
|
+
resolveAxisTickEditConfig(program, channel, { ...shared, ...(args.ticks ?? {}) }, axisId);
|
|
106
|
+
}
|
|
107
|
+
if (hasShared || Object.hasOwn(args, "labels")) {
|
|
108
|
+
resolveAxisLabelEditConfig(program, channel, { ...shared, ...(args.labels ?? {}) }, axisId);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
97
112
|
function makeCreate(channel) {
|
|
98
113
|
const operation = names(channel);
|
|
99
114
|
|