ggaction 0.0.23 → 0.0.26
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 +65 -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/knowledge/task-resolver.js +1 -1
- package/package.json +1 -1
- package/src/accessibility.js +13 -0
- package/src/actions/canvas/actions.js +2 -1
- package/src/actions/canvas/fitting.js +68 -13
- package/src/actions/charts/beeswarm.js +2 -1
- package/src/actions/charts/ecdf.js +2 -1
- package/src/actions/charts/endpoints.js +25 -16
- package/src/actions/charts/heatmap.js +6 -4
- package/src/actions/charts/missing.js +17 -0
- package/src/actions/charts/radar.js +70 -33
- package/src/actions/charts/raincloud.js +18 -4
- package/src/actions/charts/rug-strip.js +2 -2
- package/src/actions/charts/shared.js +17 -0
- package/src/actions/composition/actions.js +24 -7
- package/src/actions/composition/adjacent.js +90 -0
- package/src/actions/composition/index.js +11 -1
- package/src/actions/composition/layer.js +42 -0
- package/src/actions/composition/layerPlan.js +176 -0
- package/src/actions/composition/targets.js +105 -0
- package/src/actions/data/basicBin2d.js +1 -1
- package/src/actions/data/bin2d.js +2 -2
- package/src/actions/data/derived.js +3 -0
- package/src/actions/data/filter.js +1 -1
- package/src/actions/data/raincloudFilter.js +3 -1
- package/src/actions/encodings/appearance.js +26 -7
- package/src/actions/encodings/channels.js +1 -0
- package/src/actions/encodings/color/continuous.js +37 -34
- package/src/actions/encodings/color/index.js +57 -43
- package/src/actions/encodings/color/policy.js +15 -0
- package/src/actions/encodings/density/resolve.js +3 -2
- package/src/actions/encodings/density.js +2 -2
- package/src/actions/encodings/offset.js +2 -0
- package/src/actions/encodings/pathOrder.js +2 -0
- package/src/actions/encodings/position/areaAlignment.js +42 -0
- package/src/actions/encodings/position/index.js +37 -3
- package/src/actions/encodings/position/policies/line.js +1 -1
- package/src/actions/encodings/ranged.js +4 -2
- package/src/actions/encodings/remove.js +13 -10
- package/src/actions/encodings/ruleAppearance.js +14 -2
- package/src/actions/facets/actions.js +1 -1
- package/src/actions/guides/axes/axes.js +2 -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 +2 -537
- package/src/actions/guides/axes/parallel/lifecycle.js +11 -4
- package/src/actions/guides/axes/parallel/policy.js +5 -2
- package/src/actions/guides/axes/parallel/resolve.js +3 -1
- package/src/actions/guides/axes/parallel.js +9 -4
- 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/guides/facadeLegend.js +3 -2
- package/src/actions/guides/grids/grid.js +3 -2
- package/src/actions/guides/grids/resolve.js +2 -1
- package/src/actions/guides/guides.js +3 -2
- package/src/actions/guides/legends/automatic.js +28 -0
- package/src/actions/guides/legends/blocks.js +15 -21
- package/src/actions/guides/legends/categorical/actions.js +1 -1
- package/src/actions/guides/legends/categorical/components.js +4 -3
- package/src/actions/guides/legends/categorical/layout.js +5 -2
- package/src/actions/guides/legends/categorical/options.js +9 -14
- package/src/actions/guides/legends/categorical/recipes.js +3 -0
- package/src/actions/guides/legends/categorical/resolve.js +7 -5
- package/src/actions/guides/legends/colorSize.js +51 -14
- package/src/actions/guides/legends/continuous/common.js +1 -1
- package/src/actions/guides/legends/continuous/gradient.js +1 -1
- package/src/actions/guides/legends/continuous/opacity.js +2 -0
- package/src/actions/guides/legends/edit.js +1 -1
- package/src/actions/guides/legends/focused.js +2 -2
- package/src/actions/guides/legends/group.js +1 -1
- package/src/actions/guides/legends/lane.js +1 -1
- package/src/actions/guides/legends/lifecycle.js +2 -2
- package/src/actions/guides/legends/remove.js +18 -4
- package/src/actions/guides/legends/size.js +1 -1
- package/src/actions/guides/legends/transition.js +15 -6
- package/src/actions/guides/polar/axes/facade.js +4 -2
- package/src/actions/guides/polar/axes/labels.js +6 -1
- package/src/actions/guides/polar/axes/shared.js +4 -4
- package/src/actions/guides/polar/axes/titles.js +13 -2
- package/src/actions/guides/polar/grids.js +6 -3
- package/src/actions/guides/tickValues.js +7 -1
- package/src/actions/index.js +2 -0
- package/src/actions/marks/area/actions.js +9 -2
- package/src/actions/marks/area/materialize.js +1 -1
- package/src/actions/marks/bar/materialize.js +4 -4
- package/src/actions/marks/lifecycle.js +14 -0
- package/src/actions/marks/line/actions.js +3 -0
- package/src/actions/marks/line/materialize.js +4 -2
- package/src/actions/marks/point/edit.js +2 -0
- package/src/actions/marks/point/jitter.js +2 -0
- package/src/actions/marks/point/materialize.js +5 -4
- package/src/actions/marks/point/packing.js +2 -0
- package/src/actions/marks/references.js +5 -3
- package/src/actions/marks/remove.js +16 -0
- package/src/actions/marks/shared.js +2 -1
- package/src/actions/marks/text/actions.js +5 -2
- package/src/actions/overlays/index.js +168 -0
- package/src/actions/overlays/snapshot.js +45 -0
- package/src/actions/primitives/editGraphics.js +21 -27
- package/src/actions/primitives/semanticAction.js +4 -5
- package/src/actions/primitives/semanticValidation/guide.js +1 -0
- package/src/actions/primitives/semanticValidation/layer.js +7 -0
- package/src/actions/scales/consumers/common.js +10 -0
- package/src/actions/scales/consumers/families.js +1 -1
- package/src/actions/scales/preview.js +1 -1
- package/src/actions/selection/actions.js +63 -11
- package/src/actions/titles/actions.js +3 -14
- package/src/actions/titles/resolve.js +23 -10
- package/src/core/action.js +41 -25
- package/src/core/compositionScales.js +12 -0
- package/src/core/compositionState.js +20 -3
- package/src/core/facetGap.js +44 -0
- package/src/core/immutable.js +11 -6
- package/src/core/programState.js +16 -0
- package/src/core/resourceReferences.js +19 -3
- package/src/core/validation.js +7 -0
- package/src/core/vocabulary.js +8 -2
- package/src/grammar/actionTargets.js +24 -0
- package/src/grammar/axisTextPlacement.js +1 -1
- package/src/grammar/bin2d.js +41 -4
- package/src/grammar/colorSizeLegend.js +56 -5
- package/src/grammar/displayLabels.js +10 -0
- package/src/grammar/facetHeaders.js +1 -1
- package/src/grammar/facets/dependencies.js +10 -3
- package/src/grammar/facets/guides.js +2 -1
- package/src/grammar/facets/legendIntent.js +7 -5
- package/src/grammar/legendBlocks.js +1 -1
- package/src/grammar/legendCombinations.js +1 -1
- package/src/grammar/legendGroups.js +1 -1
- package/src/grammar/legendItemsOffset.js +12 -0
- package/src/grammar/legendOverflow.js +25 -0
- package/src/grammar/lineSeries.js +1 -1
- package/src/grammar/markSelection.js +23 -0
- package/src/grammar/overlay.js +51 -0
- package/src/grammar/parallelCoordinates.js +1 -208
- package/src/grammar/pathCommands.js +8 -2
- package/src/grammar/pathSeries.js +1 -1
- package/src/grammar/plotComposition.js +152 -0
- package/src/grammar/pointShapes.js +18 -0
- package/src/grammar/scales/transformed.js +1 -1
- package/src/grammar/schemas/concreteGraphic.js +8 -1
- package/src/grammar/schemas/graphicBounds.js +8 -1
- package/src/grammar/schemas/graphicTree.js +18 -2
- package/src/grammar/schemas/semanticPath.js +3 -0
- package/src/grammar/strokeStyle.js +14 -8
- package/src/grammar/text.js +72 -0
- package/src/inspection.js +1 -1
- package/src/inspectionDescriptors.js +1 -1
- package/src/layout/composition.js +32 -10
- package/src/layout/compositionLegend.js +91 -0
- package/src/layout/facetContentFlow.js +15 -7
- package/src/layout/facetHeaderTitles.js +1 -1
- package/src/layout/facets.js +1 -1
- package/src/layout/labels.js +55 -27
- package/src/layout/legend.js +3 -2
- package/src/layout/legendGroup.js +1 -1
- package/src/layout/legendItems.js +7 -6
- package/src/layout/legendLane.js +1 -1
- package/src/layout/legendText.js +1 -1
- package/src/layout/title.js +1 -1
- package/src/materialization/adjacentSource.js +27 -0
- package/src/materialization/composition.js +165 -52
- package/src/materialization/compositionLegends.js +120 -0
- package/src/materialization/compositionScales.js +125 -0
- package/src/materialization/compositionTitle.js +96 -0
- package/src/materialization/facetGuides/edit.js +22 -2
- package/src/materialization/facetGuides/layout.js +1 -1
- package/src/materialization/facetGuides/owner.js +4 -3
- package/src/materialization/facetGuides/ownerLayout.js +12 -6
- package/src/materialization/facetGuides/placement.js +19 -22
- package/src/materialization/facetGuides/preparation.js +17 -5
- package/src/materialization/facetHeaders.js +5 -1
- package/src/materialization/facets.js +1 -1
- package/src/materialization/guides/layout.js +1 -1
- package/src/materialization/guides/resources.js +1 -1
- package/src/materialization/guides/visibility.js +105 -0
- package/src/materialization/legendClip.js +1 -1
- package/src/materialization/legendGroups.js +1 -1
- package/src/materialization/legendItemsOffset.js +68 -0
- package/src/materialization/legendSuppression.js +22 -0
- package/src/materialization/legends.js +3 -2
- package/src/materialization/marks/capabilities.js +4 -0
- package/src/materialization/marks/index.js +5 -1
- package/src/materialization/marks/pathOrder.js +1 -1
- package/src/materialization/marks/policies.js +2 -1
- package/src/materialization/overlayItems.js +154 -0
- package/src/materialization/planner.js +19 -4
- package/src/materialization/scales/policies/binnedPosition.js +43 -5
- package/src/materialization/scales/policies/series.js +12 -4
- package/src/materialization/scales/resolve.js +2 -0
- package/src/materialization/selection/items/bar.js +21 -3
- package/src/materialization/selection/items/common.js +18 -5
- package/src/materialization/selection/items/overlay.js +15 -0
- package/src/materialization/selection/items/path.js +1 -0
- package/src/materialization/selection/items/point.js +2 -3
- package/src/materialization/selection/items/tick.js +3 -12
- package/src/materialization/selection/path.js +2 -2
- package/src/materialization/selection/policies/bar.js +2 -0
- package/src/materialization/selection/policies/index.js +35 -1
- package/src/materialization/selection/policies/point.js +2 -0
- package/src/materialization/selection/policies/text.js +17 -0
- package/src/materialization/selection/rows.js +103 -0
- package/src/materialization/selection/state.js +19 -3
- package/src/materialization/selection/styles.js +8 -0
- package/src/materialization/text.js +6 -489
- package/src/materialization/textGeometry.js +503 -0
- package/src/materialization/textLines.js +20 -0
- package/src/persistence/validation.js +116 -25
- package/src/renderers/canvas/index.js +12 -7
- package/src/renderers/canvas/text.js +5 -5
- package/src/renderers/nativeBackend.js +25 -0
- package/src/renderers/pdf.js +3 -3
- package/src/renderers/png.js +4 -3
- package/src/renderers/svgSerialization.js +1 -643
- package/src/scene.js +5 -3
- package/src/selectors/plotComposition.js +115 -0
- package/src/selectors/selectionTargets.js +19 -0
- package/src/version.js +1 -1
- package/types/basic.d.ts +4 -2
- package/types/index.d.ts +14 -0
- package/types/inspection.d.ts +34 -0
- package/types/program.d.ts +572 -372
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { materializeHardTextLines } from "../../../materialization/textLines.js";
|
|
1
2
|
import { closedAction } from "../../../core/action.js";
|
|
2
3
|
import { validateGeneratedItemLimit } from "../../../core/validation.js";
|
|
3
4
|
import { resolvePlotGraphicPlacement } from "../../../materialization/graphicHierarchy.js";
|
|
4
5
|
import { requireParallelAxisLayer, resolveParallelAxisTarget, resolveStyledParallelAxes } from "./parallel/resolve.js";
|
|
5
6
|
import { PARALLEL_AXIS_GRAPHICS, PARALLEL_AXIS_PARTS, hasParallelAxisParts } from "./parallel/policy.js";
|
|
6
7
|
import { registerParallelAxisLifecycleActions } from "./parallel/lifecycle.js";
|
|
8
|
+
import { withGuideLayoutValidation } from "../../../materialization/guides/layout.js";
|
|
7
9
|
|
|
8
10
|
function uniform(values) {
|
|
9
11
|
return values.length > 0 && values.every(value => value === values[0]) ? values[0] : values;
|
|
@@ -25,7 +27,7 @@ function collectionProperties(part, axes, bounds) {
|
|
|
25
27
|
fontSize: uniform(axes.map(axis => axis.config.title.fontSize)),
|
|
26
28
|
fontFamily: uniform(axes.map(axis => axis.config.title.fontFamily)),
|
|
27
29
|
fontWeight: uniform(axes.map(axis => axis.config.title.fontWeight)),
|
|
28
|
-
textAlign: "center", textBaseline: "middle"
|
|
30
|
+
textAlign: uniform(axes.map(axis => axis.config.title.align ?? "center")), textBaseline: "middle"
|
|
29
31
|
};
|
|
30
32
|
const rows = axes.flatMap(axis => axis[part].values.map((value, index) => ({
|
|
31
33
|
x: axis.x, y: axis[part].y[index], text: axis[part].text?.[index], config: axis.config[part]
|
|
@@ -43,14 +45,14 @@ function collectionProperties(part, axes, bounds) {
|
|
|
43
45
|
fontSize: uniform(rows.map(row => row.config.fontSize)),
|
|
44
46
|
fontFamily: uniform(rows.map(row => row.config.fontFamily)),
|
|
45
47
|
fontWeight: uniform(rows.map(row => row.config.fontWeight)),
|
|
46
|
-
textAlign: "right", textBaseline: "middle"
|
|
48
|
+
textAlign: uniform(rows.map(row => row.config.align ?? "right")), textBaseline: "middle"
|
|
47
49
|
};
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
export const rematerializeParallelAxes = /* @__PURE__ */ closedAction({
|
|
51
53
|
op: "rematerializeParallelAxes",
|
|
52
54
|
description: "Recompute concrete Parallel dimension axes."
|
|
53
|
-
}, ["target"], function (args = {}) {
|
|
55
|
+
}, ["target"], withGuideLayoutValidation(function (args = {}) {
|
|
54
56
|
|
|
55
57
|
const stored = this.semanticSpec.guides.axis?.parallel;
|
|
56
58
|
if (stored === undefined) throw new Error("rematerializeParallelAxes requires existing Parallel axes.");
|
|
@@ -64,6 +66,8 @@ export const rematerializeParallelAxes = /* @__PURE__ */ closedAction({
|
|
|
64
66
|
const properties = Object.fromEntries(PARALLEL_AXIS_PARTS.map(part => {
|
|
65
67
|
const selected = axes.filter(axis => axis.config[part] !== undefined);
|
|
66
68
|
const value = collectionProperties(part, selected, bounds);
|
|
69
|
+
if (selected.some(axis => axis.config.visibility !== undefined)) value.opacity = selected.flatMap(axis => Array.from({ length: ["line", "title"].includes(part) ? 1 : axis[part].values.length },
|
|
70
|
+
() => axis.config.visibility?.all === false || axis.config.visibility?.[part] === false ? 0 : 1));
|
|
67
71
|
validateGeneratedItemLimit(value.length, "Parallel axis item count");
|
|
68
72
|
return [part, { enabled: selected.length > 0, value }];
|
|
69
73
|
}));
|
|
@@ -94,9 +98,10 @@ export const rematerializeParallelAxes = /* @__PURE__ */ closedAction({
|
|
|
94
98
|
for (const [property, item] of Object.entries(value)) {
|
|
95
99
|
next = next.editGraphics({ target: id, property, value: item });
|
|
96
100
|
}
|
|
101
|
+
if (["title", "labels"].includes(part)) next = materializeHardTextLines(next, id);
|
|
97
102
|
}
|
|
98
103
|
return next._withGuideConfig("parallel", "axes", { target, scales, ...configs });
|
|
99
|
-
});
|
|
104
|
+
}));
|
|
100
105
|
|
|
101
106
|
export function registerParallelAxisActions(ProgramClass) {
|
|
102
107
|
ProgramClass.prototype.rematerializeParallelAxes = rematerializeParallelAxes;
|
|
@@ -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
|
|
|
@@ -141,6 +141,38 @@ function geometry(program, channel, config, axisId = channel) {
|
|
|
141
141
|
return resolved;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
export function resolveAxisTickEditConfig(program, channel, args, axisId = channel) {
|
|
145
|
+
const op = channel === "x" ? "editXAxisTicks" : "editYAxisTicks";
|
|
146
|
+
validateOptions(args, op, false);
|
|
147
|
+
const previous = program.guideConfigs.axis?.[axisId]?.ticks;
|
|
148
|
+
if (!previous) throw new Error(`${op} requires tick configuration.`);
|
|
149
|
+
const explicitMode = Object.hasOwn(args, "values") || Object.hasOwn(args, "count");
|
|
150
|
+
const inferredValues = !explicitMode && previous.inferredValues === true
|
|
151
|
+
? inferHistogramBoundaries(program, channel, previous.scale) ??
|
|
152
|
+
(["ordinal", "band", "point"].includes(
|
|
153
|
+
program.resolvedScales[previous.scale]?.type
|
|
154
|
+
)
|
|
155
|
+
? program.resolvedScales[previous.scale].domain
|
|
156
|
+
: undefined)
|
|
157
|
+
: undefined;
|
|
158
|
+
const mode = Object.hasOwn(args, "values") || inferredValues !== undefined
|
|
159
|
+
? "values"
|
|
160
|
+
: Object.hasOwn(args, "count") ? "count" : previous.mode;
|
|
161
|
+
const config = patchGuideDash({
|
|
162
|
+
...previous,
|
|
163
|
+
...args,
|
|
164
|
+
...(inferredValues === undefined ? {} : { values: inferredValues }),
|
|
165
|
+
...(explicitMode ? { inferredValues: false } : {}),
|
|
166
|
+
mode
|
|
167
|
+
}, args);
|
|
168
|
+
if (mode === "values") {
|
|
169
|
+
delete config.count;
|
|
170
|
+
if (!Object.hasOwn(args, "rounding")) delete config.rounding;
|
|
171
|
+
} else delete config.values;
|
|
172
|
+
validateConfig(channel, config);
|
|
173
|
+
return config;
|
|
174
|
+
}
|
|
175
|
+
|
|
144
176
|
function makeEdit(channel) {
|
|
145
177
|
const op = channel === "x" ? "editXAxisTicks" : "editYAxisTicks";
|
|
146
178
|
return action({ op, description: `Edit concrete ${channel}-axis ticks.` }, withGuideLayoutValidation(function (args = {}) {
|
|
@@ -149,32 +181,9 @@ function makeEdit(channel) {
|
|
|
149
181
|
const axisId = selected.id; args = selected.options;
|
|
150
182
|
const id = axisGraphicIds(channel, axisId)[1];
|
|
151
183
|
if (this.graphicSpec.objects[id]?.type !== "line") throw new Error(`${op} requires existing axis ticks.`);
|
|
184
|
+
const config = resolveAxisTickEditConfig(this, channel, args, axisId);
|
|
152
185
|
const previous = this.guideConfigs.axis?.[axisId]?.ticks;
|
|
153
|
-
if (!previous) throw new Error(`${op} requires tick configuration.`);
|
|
154
186
|
const explicitMode = Object.hasOwn(args, "values") || Object.hasOwn(args, "count");
|
|
155
|
-
const inferredValues = !explicitMode && previous.inferredValues === true
|
|
156
|
-
? inferHistogramBoundaries(this, channel, previous.scale) ??
|
|
157
|
-
(["ordinal", "band", "point"].includes(
|
|
158
|
-
this.resolvedScales[previous.scale]?.type
|
|
159
|
-
)
|
|
160
|
-
? this.resolvedScales[previous.scale].domain
|
|
161
|
-
: undefined)
|
|
162
|
-
: undefined;
|
|
163
|
-
const mode = Object.hasOwn(args, "values") || inferredValues !== undefined
|
|
164
|
-
? "values"
|
|
165
|
-
: Object.hasOwn(args, "count") ? "count" : previous.mode;
|
|
166
|
-
const config = patchGuideDash({
|
|
167
|
-
...previous,
|
|
168
|
-
...args,
|
|
169
|
-
...(inferredValues === undefined ? {} : { values: inferredValues }),
|
|
170
|
-
...(explicitMode ? { inferredValues: false } : {}),
|
|
171
|
-
mode
|
|
172
|
-
}, args);
|
|
173
|
-
if (mode === "values") {
|
|
174
|
-
delete config.count;
|
|
175
|
-
if (!Object.hasOwn(args, "rounding")) delete config.rounding;
|
|
176
|
-
} else delete config.values;
|
|
177
|
-
validateConfig(channel, config);
|
|
178
187
|
const resolved = geometry(this, channel, config, axisId);
|
|
179
188
|
let next = this._withGuideConfig(axisId, config).editGraphics({ target: id, property: "length", value: resolved.values.length });
|
|
180
189
|
for (const property of ["x1", "y1", "x2", "y2"]) next = next.editGraphics({ target: id, property, value: resolved[property] });
|
|
@@ -1,389 +1 @@
|
|
|
1
|
-
import { selectCartesianAxis, axisTargetArgs } from "../../../selectors/axes.js";
|
|
2
|
-
import { axisGraphicIds } from "../../../materialization/guides/resources.js";
|
|
3
|
-
import { materializeTextLines } from "../../../materialization/textLines.js";
|
|
4
|
-
import { axisTitleTextLines as titleLines, validateAxisTextPlacement, axisTitleAllowsLabelOverlap } from "../../../grammar/axisTextPlacement.js";
|
|
5
|
-
import { axisThemeTypography } from "../../theme/state.js";
|
|
6
|
-
import { canvasOverflowError } from "../../../layout/canvas.js";
|
|
7
|
-
import { isSourceOwnedText } from "../../../grammar/text.js";
|
|
8
|
-
import { withGuideLayoutValidation } from "../../../materialization/guides/layout.js";
|
|
9
|
-
import { action } from "../../../core/action.js";
|
|
10
|
-
import { validateUserId } from "../../../core/identifiers.js";
|
|
11
|
-
import {
|
|
12
|
-
validateKeys,
|
|
13
|
-
validateNonEmptyString
|
|
14
|
-
} from "../../../core/validation.js";
|
|
15
|
-
import { axisScaleBindings, resolveCartesianAxisFrameBounds } from "../../../materialization/coordinateBounds.js";
|
|
16
|
-
import {
|
|
17
|
-
isTransformedScaleType,
|
|
18
|
-
mapContinuousScaleValues,
|
|
19
|
-
mapOrdinalPositionValues
|
|
20
|
-
} from "../../../grammar/scales/index.js";
|
|
21
|
-
import { AXIS_TITLE_FONT_SIZE, DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
|
|
22
|
-
"../../../theme/defaults.js";
|
|
23
|
-
import { findDataset } from "../../../selectors/datasets.js";
|
|
24
|
-
import { findUpstreamTransform } from "../../../materialization/dataProvenance.js";
|
|
25
|
-
import { formatAggregateTitle } from "../../../grammar/aggregate.js";
|
|
26
|
-
import { finiteMidpoint } from "../../../grammar/numeric.js";
|
|
27
|
-
import {
|
|
28
|
-
defaultAxisPosition,
|
|
29
|
-
defaultAxisTitleRotation,
|
|
30
|
-
resolveAxisTitleGeometry,
|
|
31
|
-
validateAxisPosition
|
|
32
|
-
} from "./policy.js";
|
|
33
|
-
import { findCanvasGraphic, resolvePlotGraphicPlacement } from
|
|
34
|
-
"../../../materialization/graphicHierarchy.js";
|
|
35
|
-
import {
|
|
36
|
-
resolveTextBounds,
|
|
37
|
-
textBoundsFitCanvas,
|
|
38
|
-
textBoundsIntersect
|
|
39
|
-
} from "../../../core/textMetrics.js";
|
|
40
|
-
import { resolveConcreteGraphicBounds } from
|
|
41
|
-
"../../../grammar/schemas/graphicBounds.js";
|
|
42
|
-
import { validateAxisTextStyle } from "./labels.js";
|
|
43
|
-
import { resolveRotation } from "../../../grammar/rotation.js";
|
|
44
|
-
|
|
45
|
-
function titleBounds(geometry, config, text, profile) {
|
|
46
|
-
return resolveTextBounds({
|
|
47
|
-
...geometry,
|
|
48
|
-
text,
|
|
49
|
-
lines: titleLines(text, config),
|
|
50
|
-
fontSize: config.fontSize,
|
|
51
|
-
fontFamily: config.fontFamily,
|
|
52
|
-
fontWeight: config.fontWeight,
|
|
53
|
-
textAlign: config.align ?? "center",
|
|
54
|
-
textBaseline: config.baseline ?? "middle",
|
|
55
|
-
rotation: config.rotation
|
|
56
|
-
}, profile);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function resolveTitleAlong(scale, config) {
|
|
60
|
-
const discrete = ["ordinal", "band", "point"].includes(scale?.type);
|
|
61
|
-
let along;
|
|
62
|
-
if (config.at === "center") {
|
|
63
|
-
along = finiteMidpoint(scale.range[0], scale.range[1]);
|
|
64
|
-
} else if (config.at === "start" || config.at === "end") {
|
|
65
|
-
along = scale.range[config.at === "start" ? 0 : 1];
|
|
66
|
-
} else {
|
|
67
|
-
if (discrete) {
|
|
68
|
-
if (!scale.domain.includes(config.at)) throw new RangeError("Axis title at value must be inside the scale domain.");
|
|
69
|
-
along = mapOrdinalPositionValues([config.at], scale)[0];
|
|
70
|
-
} else {
|
|
71
|
-
const low = Math.min(...scale.domain), high = Math.max(...scale.domain);
|
|
72
|
-
if (config.at < low || config.at > high) throw new RangeError("Axis title at value must be inside the scale domain.");
|
|
73
|
-
along = mapContinuousScaleValues([config.at], scale)[0];
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
return along + (config.alongOffset ?? 0);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function axisLabelBounds(program, channel, axisId = channel) {
|
|
80
|
-
const id = axisGraphicIds(channel, axisId)[2], labels = program.graphicSpec.objects[id];
|
|
81
|
-
const bounds = target => resolveConcreteGraphicBounds(
|
|
82
|
-
program.graphicSpec, target, program.materializationConfigs.textMetrics
|
|
83
|
-
);
|
|
84
|
-
return labels?.items?.map(item => bounds(item.id)) ?? (labels ? [bounds(id)] : []);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function inferredTitleOffset(program, channel, config, axisId = channel) {
|
|
88
|
-
const plot = resolveCartesianAxisFrameBounds(program, channel, config.position, config.scale, axisId);
|
|
89
|
-
const canvas = findCanvasGraphic(program)?.properties;
|
|
90
|
-
const labels = program.graphicSpec.objects[axisGraphicIds(channel, axisId)[2]]
|
|
91
|
-
? resolveConcreteGraphicBounds(program.graphicSpec, axisGraphicIds(channel, axisId)[2], program.materializationConfigs.textMetrics)
|
|
92
|
-
: undefined;
|
|
93
|
-
const preferred = channel === "x" ? 42 : 52;
|
|
94
|
-
if (!plot || !canvas || !labels) return preferred;
|
|
95
|
-
const horizontal = channel === "x";
|
|
96
|
-
const positive = config.position === (horizontal ? "bottom" : "right");
|
|
97
|
-
const near = horizontal ? "top" : "left";
|
|
98
|
-
const far = horizontal ? "bottom" : "right";
|
|
99
|
-
const bounds = titleBounds(resolveAxisTitleGeometry({
|
|
100
|
-
bounds: plot,
|
|
101
|
-
channel,
|
|
102
|
-
position: config.position,
|
|
103
|
-
along: 0,
|
|
104
|
-
offset: preferred
|
|
105
|
-
}), config, program.semanticSpec.guides.axis?.[axisId]?.title ?? "", program.materializationConfigs.textMetrics);
|
|
106
|
-
const needed = positive
|
|
107
|
-
? labels[far] + 4 - bounds[near]
|
|
108
|
-
: bounds[far] - labels[near] + 4;
|
|
109
|
-
const available = positive
|
|
110
|
-
? canvas[horizontal ? "height" : "width"] - bounds[far]
|
|
111
|
-
: bounds[near];
|
|
112
|
-
// Font ink can extend beyond the one-em layout box, especially after
|
|
113
|
-
// rotating a middle-baseline title. Keep inferred edge-clamped placement
|
|
114
|
-
// inside the Canvas; explicit offsets retain their exact geometry.
|
|
115
|
-
const inkClearance = channel === "y" &&
|
|
116
|
-
Math.abs(Math.cos(config.rotation)) < 1e-10
|
|
117
|
-
? config.fontSize / 4 : 0;
|
|
118
|
-
if (program.materializationConfigs.canvas?.plot !== undefined) {
|
|
119
|
-
return preferred + Math.max(0, needed);
|
|
120
|
-
}
|
|
121
|
-
const legacyAdjustment = Math.min(available, Math.max(0, needed));
|
|
122
|
-
if (inkClearance === 0) return preferred + legacyAdjustment;
|
|
123
|
-
const adjustment = Math.min(available - inkClearance, Math.max(0, needed));
|
|
124
|
-
const candidateBounds = titleBounds(resolveAxisTitleGeometry({
|
|
125
|
-
bounds: plot, channel, position: config.position,
|
|
126
|
-
along: resolveTitleAlong(program.resolvedScales[config.scale], config),
|
|
127
|
-
offset: preferred + adjustment
|
|
128
|
-
}), config, program.semanticSpec.guides.axis?.[axisId]?.title ?? "", program.materializationConfigs.textMetrics);
|
|
129
|
-
// Optional ink reserve must not introduce a label collision.
|
|
130
|
-
return preferred + (axisLabelBounds(program, channel, axisId).some(label => label && textBoundsIntersect(candidateBounds, label))
|
|
131
|
-
? legacyAdjustment : adjustment);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const CREATE_OPTIONS = ["target",
|
|
135
|
-
"text", "scale", "position", "at", "offset", "rotation", "color",
|
|
136
|
-
"fontSize", "fontFamily", "fontWeight", "alongOffset", "align", "baseline", "overlap"
|
|
137
|
-
];
|
|
138
|
-
const EDIT_OPTIONS = CREATE_OPTIONS.filter(key => key !== "scale");
|
|
139
|
-
const DEFAULTS = {
|
|
140
|
-
color: DEFAULT_COLORS.text,
|
|
141
|
-
fontSize: AXIS_TITLE_FONT_SIZE,
|
|
142
|
-
fontFamily: DEFAULT_FONT_FAMILY,
|
|
143
|
-
fontWeight: 600
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
function validateText(text) {
|
|
147
|
-
return validateNonEmptyString(text, "Axis title text");
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function validateConfig(channel, config) {
|
|
151
|
-
validateAxisPosition(channel, config.position);
|
|
152
|
-
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.");
|
|
153
|
-
validateAxisTextPlacement(config, { title: true });
|
|
154
|
-
if (!Number.isFinite(config.rotation)) throw new TypeError("Axis title rotation must be finite radians.");
|
|
155
|
-
validateAxisTextStyle(config, "Axis title");
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export function inferAxisTitleText(program, channel, scaleId) {
|
|
159
|
-
const titles = new Set();
|
|
160
|
-
const primaryTitles = new Set();
|
|
161
|
-
for (const layer of program.semanticSpec.layers) {
|
|
162
|
-
if (isSourceOwnedText(layer)) continue;
|
|
163
|
-
// Reference statistics reuse the source axis title.
|
|
164
|
-
if (program.markConfigs[layer.id]?.statisticalReference !== undefined) continue;
|
|
165
|
-
const primary = layer.encoding?.[channel];
|
|
166
|
-
const encoding = layer.mark?.type === "area" && Object.hasOwn(primary ?? {}, "datum")
|
|
167
|
-
? layer.encoding?.[`${channel}2`] : primary;
|
|
168
|
-
if (channel === "radius" && layer.mark?.type === "arc" &&
|
|
169
|
-
encoding?.scale === scaleId && encoding.aggregate === "count" && encoding.field === undefined) {
|
|
170
|
-
titles.add(encoding.title ?? "count");
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
if (
|
|
174
|
-
encoding?.scale === scaleId &&
|
|
175
|
-
typeof encoding.field === "string" &&
|
|
176
|
-
encoding.field.length
|
|
177
|
-
) {
|
|
178
|
-
const dataset = findDataset(program, layer.data);
|
|
179
|
-
const transform = findUpstreamTransform(program, dataset,
|
|
180
|
-
["density", "ecdf", "interval", "boxSummary", "gradientProfile", "summary"]);
|
|
181
|
-
let derivedTitle;
|
|
182
|
-
if (transform?.type === "density") {
|
|
183
|
-
derivedTitle = encoding.field === transform.as?.[0]
|
|
184
|
-
? transform.field
|
|
185
|
-
: encoding.field === transform.as?.[1]
|
|
186
|
-
? "Density"
|
|
187
|
-
: undefined;
|
|
188
|
-
} else if (transform?.type === "ecdf") {
|
|
189
|
-
derivedTitle = encoding.field === transform.as.value ? transform.field
|
|
190
|
-
: encoding.field === transform.as.probability ? "Cumulative probability"
|
|
191
|
-
: encoding.field === transform.as.cumulative ? "Cumulative count" : undefined;
|
|
192
|
-
} else if (transform?.type === "interval" &&
|
|
193
|
-
Object.values(transform.as).includes(encoding.field)) {
|
|
194
|
-
derivedTitle = `${transform.center}(${transform.field})`;
|
|
195
|
-
} else if (transform?.type === "boxSummary" &&
|
|
196
|
-
Object.values(transform.as).includes(encoding.field)) {
|
|
197
|
-
derivedTitle = transform.field;
|
|
198
|
-
} else if (transform?.type === "gradientProfile" &&
|
|
199
|
-
[transform.as.lower, transform.as.upper, transform.as.center].includes(encoding.field)) {
|
|
200
|
-
// Profile bounds and the optional center share one source measure.
|
|
201
|
-
derivedTitle = transform.field;
|
|
202
|
-
} else if (transform?.type === "summary") {
|
|
203
|
-
const aggregate = transform.aggregates.find(value => value.as === encoding.field);
|
|
204
|
-
if (aggregate) derivedTitle = formatAggregateTitle(aggregate.op, aggregate.field);
|
|
205
|
-
}
|
|
206
|
-
const title = encoding.title ?? derivedTitle ?? (encoding.aggregate === undefined
|
|
207
|
-
? encoding.field
|
|
208
|
-
: formatAggregateTitle(encoding.aggregate, encoding.field));
|
|
209
|
-
titles.add(title);
|
|
210
|
-
if (layer.mark?.type === "point") primaryTitles.add(title);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
if (primaryTitles.size === 1) return [...primaryTitles][0];
|
|
214
|
-
if (titles.size === 0 && axisScaleBindings(program, scaleId).some(binding => binding.channel === channel)) return scaleId;
|
|
215
|
-
if (titles.size !== 1) throw new Error(`Axis title text cannot be inferred for scale "${scaleId}".`);
|
|
216
|
-
return [...titles][0];
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function resolveGeometry(program, channel, config, axisId = channel) {
|
|
220
|
-
const scale = program.resolvedScales[config.scale];
|
|
221
|
-
const bounds = resolveCartesianAxisFrameBounds(program, channel, config.position, config.scale, axisId);
|
|
222
|
-
if ((
|
|
223
|
-
!["linear", "time", "ordinal", "band", "point"].includes(scale?.type) &&
|
|
224
|
-
!isTransformedScaleType(scale?.type)
|
|
225
|
-
) || !bounds) throw new Error("Axis title requires a supported resolved scale and Canvas bounds.");
|
|
226
|
-
const along = resolveTitleAlong(scale, config);
|
|
227
|
-
const geometry = resolveAxisTitleGeometry({
|
|
228
|
-
bounds,
|
|
229
|
-
channel,
|
|
230
|
-
position: config.position,
|
|
231
|
-
along,
|
|
232
|
-
offset: config.offset
|
|
233
|
-
});
|
|
234
|
-
const resolvedBounds = titleBounds(
|
|
235
|
-
geometry,
|
|
236
|
-
config,
|
|
237
|
-
program.semanticSpec.guides.axis?.[axisId]?.title ?? ""
|
|
238
|
-
, program.materializationConfigs.textMetrics);
|
|
239
|
-
const canvas = findCanvasGraphic(program)?.properties;
|
|
240
|
-
if (!canvas || !textBoundsFitCanvas(resolvedBounds, canvas)) {
|
|
241
|
-
throw canvasOverflowError(`The ${channel}-axis title does not fit the Canvas margin.`, [resolvedBounds], canvas);
|
|
242
|
-
}
|
|
243
|
-
const labelBounds = axisLabelBounds(program, channel, axisId);
|
|
244
|
-
if (!axisTitleAllowsLabelOverlap(config) && labelBounds.some(bounds => bounds && textBoundsIntersect(resolvedBounds, bounds))) {
|
|
245
|
-
throw new Error(`The ${channel}-axis title overlaps the axis labels.`);
|
|
246
|
-
}
|
|
247
|
-
return geometry;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function names(channel) {
|
|
251
|
-
const prefix = channel === "x" ? "X" : "Y";
|
|
252
|
-
return { create: `create${prefix}AxisTitle`, edit: `edit${prefix}AxisTitle` };
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function makeEdit(channel) {
|
|
256
|
-
const operation = names(channel);
|
|
257
|
-
return action({ op: operation.edit, description: `Edit the ${channel}-axis title.` }, withGuideLayoutValidation(function (args = {}) {
|
|
258
|
-
validateKeys(args, EDIT_OPTIONS, operation.edit);
|
|
259
|
-
const selected = selectCartesianAxis(this, channel, args, { create: false });
|
|
260
|
-
const axisId = selected.id; args = selected.options;
|
|
261
|
-
const graphic = axisGraphicIds(channel, axisId)[3];
|
|
262
|
-
validateAxisTextPlacement(args, { title: true });
|
|
263
|
-
if (this.graphicSpec.objects[graphic]?.type !== "text") throw new Error(`${operation.edit} requires an existing axis title.`);
|
|
264
|
-
const previous = this.guideConfigs.axis?.[axisId]?.title;
|
|
265
|
-
if (!previous) throw new Error(`${operation.edit} requires title configuration.`);
|
|
266
|
-
const { text, rotation: requestedRotation, ...appearance } = args;
|
|
267
|
-
const explicitText = Object.hasOwn(args, "text");
|
|
268
|
-
const explicitRotation = Object.hasOwn(args, "rotation");
|
|
269
|
-
const position = appearance.position ?? previous.position;
|
|
270
|
-
const inferredRotation = explicitRotation
|
|
271
|
-
? false
|
|
272
|
-
: previous.inferredRotation === true;
|
|
273
|
-
const config = {
|
|
274
|
-
...previous,
|
|
275
|
-
...appearance,
|
|
276
|
-
position,
|
|
277
|
-
rotation: inferredRotation
|
|
278
|
-
? defaultAxisTitleRotation(channel, position)
|
|
279
|
-
: explicitRotation
|
|
280
|
-
? resolveRotation(requestedRotation, "Axis title rotation")
|
|
281
|
-
: previous.rotation,
|
|
282
|
-
inferredRotation,
|
|
283
|
-
inferredOffset: Object.hasOwn(args, "offset")
|
|
284
|
-
? false
|
|
285
|
-
: previous.inferredOffset === true,
|
|
286
|
-
...(explicitText ? { inferredText: false } : {})
|
|
287
|
-
};
|
|
288
|
-
validateConfig(channel, config);
|
|
289
|
-
let next = this;
|
|
290
|
-
const resolvedTitle = explicitText
|
|
291
|
-
? validateText(text)
|
|
292
|
-
: config.inferredText === true
|
|
293
|
-
? inferAxisTitleText(this, channel, config.scale)
|
|
294
|
-
: this.semanticSpec.guides.axis?.[axisId]?.title;
|
|
295
|
-
if (resolvedTitle !== this.semanticSpec.guides.axis?.[axisId]?.title) {
|
|
296
|
-
next = next.editSemantic({
|
|
297
|
-
property: `guide.axis.${axisId}.title`,
|
|
298
|
-
value: validateText(resolvedTitle)
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
const resolvedText = validateText(next.semanticSpec.guides.axis?.[axisId]?.title);
|
|
302
|
-
if (config.inferredOffset) {
|
|
303
|
-
config.offset = inferredTitleOffset(next, channel, config, axisId);
|
|
304
|
-
}
|
|
305
|
-
const geometry = resolveGeometry(next, channel, config, axisId);
|
|
306
|
-
next = next._withGuideConfig(axisId, "title", config);
|
|
307
|
-
const properties = {
|
|
308
|
-
x: geometry.x, y: geometry.y, text: resolvedText, fill: config.color,
|
|
309
|
-
fontSize: config.fontSize, fontFamily: config.fontFamily,
|
|
310
|
-
fontWeight: config.fontWeight, textAlign: config.align ?? "center", textBaseline: config.baseline ?? "middle",
|
|
311
|
-
rotation: config.rotation
|
|
312
|
-
};
|
|
313
|
-
for (const [property, value] of Object.entries(properties)) next = next.editGraphics({ target: graphic, property, value });
|
|
314
|
-
return materializeTextLines(next, graphic, titleLines(resolvedText, config));
|
|
315
|
-
}));
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
const editXAxisTitle = /* @__PURE__ */ makeEdit("x");
|
|
319
|
-
const editYAxisTitle = /* @__PURE__ */ makeEdit("y");
|
|
320
|
-
|
|
321
|
-
function makeCreate(channel) {
|
|
322
|
-
const operation = names(channel);
|
|
323
|
-
return action({ op: operation.create, description: `Create the ${channel}-axis title.` }, withGuideLayoutValidation(function (args = {}) {
|
|
324
|
-
validateKeys(args, CREATE_OPTIONS, operation.create);
|
|
325
|
-
const selected = selectCartesianAxis(this, channel, args, { create: true });
|
|
326
|
-
const axisId = selected.id; args = selected.options;
|
|
327
|
-
const graphic = axisGraphicIds(channel, axisId)[3];
|
|
328
|
-
validateAxisTextPlacement(args, { title: true });
|
|
329
|
-
const {
|
|
330
|
-
text: requestedText,
|
|
331
|
-
scale: requestedScale,
|
|
332
|
-
position: requestedPosition,
|
|
333
|
-
rotation: requestedRotation,
|
|
334
|
-
...appearance
|
|
335
|
-
} = args;
|
|
336
|
-
const scale = validateUserId(requestedScale ?? channel, "Scale id");
|
|
337
|
-
const guideScale = this.semanticSpec.guides.axis?.[axisId]?.scale;
|
|
338
|
-
if (guideScale && guideScale !== scale) throw new Error(`${operation.create} conflicts with the existing axis scale.`);
|
|
339
|
-
if (this.graphicSpec.objects[graphic]) throw new Error(`${operation.create} requires a missing axis title.`);
|
|
340
|
-
const inferredText = !Object.hasOwn(args, "text");
|
|
341
|
-
const text = validateText(
|
|
342
|
-
requestedText ?? inferAxisTitleText(this, channel, scale)
|
|
343
|
-
);
|
|
344
|
-
const position = requestedPosition ?? defaultAxisPosition(channel);
|
|
345
|
-
const inferredRotation = !Object.hasOwn(args, "rotation");
|
|
346
|
-
const inferredOffset = !Object.hasOwn(args, "offset");
|
|
347
|
-
const config = {
|
|
348
|
-
scale,
|
|
349
|
-
inferredText,
|
|
350
|
-
position,
|
|
351
|
-
at: "center",
|
|
352
|
-
offset: channel === "x" ? 42 : 52,
|
|
353
|
-
rotation: inferredRotation
|
|
354
|
-
? defaultAxisTitleRotation(channel, position)
|
|
355
|
-
: resolveRotation(requestedRotation, "Axis title rotation"),
|
|
356
|
-
inferredRotation,
|
|
357
|
-
inferredOffset,
|
|
358
|
-
color: DEFAULTS.color,
|
|
359
|
-
fontSize: DEFAULTS.fontSize,
|
|
360
|
-
fontFamily: DEFAULTS.fontFamily,
|
|
361
|
-
fontWeight: DEFAULTS.fontWeight,
|
|
362
|
-
...axisThemeTypography(this.materializationConfigs.theme, "title"),
|
|
363
|
-
...appearance
|
|
364
|
-
};
|
|
365
|
-
let next = this
|
|
366
|
-
.editSemantic({ property: `guide.axis.${axisId}.scale`, value: scale })
|
|
367
|
-
.editSemantic({ property: `guide.axis.${axisId}.title`, value: text });
|
|
368
|
-
if (inferredOffset) config.offset = inferredTitleOffset(next, channel, config, axisId);
|
|
369
|
-
validateConfig(channel, config);
|
|
370
|
-
resolveGeometry(next, channel, config, axisId);
|
|
371
|
-
return next
|
|
372
|
-
.createGraphics({
|
|
373
|
-
id: graphic,
|
|
374
|
-
type: "text",
|
|
375
|
-
...resolvePlotGraphicPlacement(this)
|
|
376
|
-
})
|
|
377
|
-
._withGuideConfig(axisId, "title", config)[operation.edit](axisId === channel ? undefined : axisTargetArgs(axisId, channel));
|
|
378
|
-
}));
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
const createXAxisTitle = /* @__PURE__ */ makeCreate("x");
|
|
382
|
-
const createYAxisTitle = /* @__PURE__ */ makeCreate("y");
|
|
383
|
-
|
|
384
|
-
export function registerAxisTitleActions(Class) {
|
|
385
|
-
Class.prototype.createXAxisTitle = createXAxisTitle;
|
|
386
|
-
Class.prototype.createYAxisTitle = createYAxisTitle;
|
|
387
|
-
Class.prototype.editXAxisTitle = editXAxisTitle;
|
|
388
|
-
Class.prototype.editYAxisTitle = editYAxisTitle;
|
|
389
|
-
}
|
|
1
|
+
import{compositionChannel as z}from"../../../selectors/plotComposition.js";import{selectCartesianAxis as E,axisTargetArgs as I}from"../../../selectors/axes.js";import{axisGraphicIds as h}from"../../../materialization/guides/resources.js";import{materializeTextLines as tt}from"../../../materialization/textLines.js";import{axisTitleTextLines as M,validateAxisTextPlacement as g,axisTitleAllowsLabelOverlap as et,isLabelRelativeTitleOffset as F,axisLabelHasText as it}from"../../../grammar/axisTextPlacement.js";import{axisThemeTypography as ot}from"../../theme/state.js";import{canvasOverflowError as j}from"../../../layout/canvas.js";import{isSourceOwnedText as st}from"../../../grammar/text.js";import{withGuideLayoutValidation as $}from"../../../materialization/guides/layout.js";import{action as L}from"../../../core/action.js";import{validateUserId as nt}from"../../../core/identifiers.js";import{validateKeys as R,validateNonEmptyString as rt}from"../../../core/validation.js";import{axisScaleBindings as at,resolveCartesianAxisFrameBounds as B}from"../../../materialization/coordinateBounds.js";import{isTransformedScaleType as lt,mapContinuousScaleValues as ft,mapOrdinalPositionValues as dt}from"../../../grammar/scales/index.js";import{AXIS_TITLE_FONT_SIZE as ct,DEFAULT_COLORS as ut,DEFAULT_FONT_FAMILY as pt}from"../../../theme/defaults.js";import{findDataset as mt}from"../../../selectors/datasets.js";import{findUpstreamTransform as xt}from"../../../materialization/dataProvenance.js";import{formatAggregateTitle as q}from"../../../grammar/aggregate.js";import{finiteMidpoint as vt}from"../../../grammar/numeric.js";import{defaultAxisPosition as yt,defaultAxisTitleRotation as G,resolveAxisTitleGeometry as S,validateAxisPosition as bt}from"./policy.js";import{findCanvasGraphic as P,resolvePlotGraphicPlacement as Tt}from"../../../materialization/graphicHierarchy.js";import{resolveTextBounds as ht,textBoundsFitCanvas as St,textBoundsIntersect as _}from"../../../core/textMetrics.js";import{resolveConcreteGraphicBounds as C}from"../../../grammar/schemas/graphicBounds.js";import{validateAxisTextStyle as wt}from"./labels.js";import{resolveRotation as N}from"../../../grammar/rotation.js";function w(t,e,i,n){return ht({...t,text:i,lines:M(i,e),fontSize:e.fontSize,fontFamily:e.fontFamily,fontWeight:e.fontWeight,textAlign:e.align??"center",textBaseline:e.baseline??"middle",rotation:e.rotation},n)}function W(t,e){const i=["ordinal","band","point"].includes(t?.type);let n;if(e.at==="center")n=vt(t.range[0],t.range[1]);else if(e.at==="start"||e.at==="end")n=t.range[e.at==="start"?0:1];else if(i){if(!t.domain.includes(e.at))throw new RangeError("Axis title at value must be inside the scale domain.");n=dt([e.at],t)[0]}else{const a=Math.min(...t.domain),s=Math.max(...t.domain);if(e.at<a||e.at>s)throw new RangeError("Axis title at value must be inside the scale domain.");n=ft([e.at],t)[0]}return n+(e.alongOffset??0)}function k(t,e,i=e){const n=h(e,i)[2],a=t.graphicSpec.objects[n],s=f=>C(t.graphicSpec,f,t.materializationConfigs.textMetrics);return a?.items?.map(f=>s(f.id))??(a?[s(n)]:[])}function D(t,e,i,n=e){const a=B(t,e,i.position,i.scale,n),s=P(t)?.properties,f=t.graphicSpec.objects[h(e,n)[2]]?C(t.graphicSpec,h(e,n)[2],t.materializationConfigs.textMetrics):void 0,o=e==="x"?42:52;if(!a||!s||!f)return o;const c=e==="x",r=i.position===(c?"bottom":"right"),d=c?"top":"left",u=c?"bottom":"right",p=w(S({bounds:a,channel:e,position:i.position,along:0,offset:o}),i,t.semanticSpec.guides.axis?.[n]?.title??"",t.materializationConfigs.textMetrics),x=r?f[u]+4-p[d]:p[u]-f[d]+4,l=r?s[c?"height":"width"]-p[u]:p[d],m=e==="y"&&Math.abs(Math.cos(i.rotation))<1e-10?i.fontSize/4:0;if(t.materializationConfigs.canvas?.plot!==void 0)return o+Math.max(0,x);const y=Math.min(l,Math.max(0,x));if(m===0)return o+y;const b=Math.min(l-m,Math.max(0,x)),v=w(S({bounds:a,channel:e,position:i.position,along:W(t.resolvedScales[i.scale],i),offset:o+b}),i,t.semanticSpec.guides.axis?.[n]?.title??"",t.materializationConfigs.textMetrics);return o+(k(t,e,n).some(T=>T&&_(v,T))?y:b)}function At(t,e,i,n,a,s){const f=t.graphicSpec.objects[h(e,n)[2]];if(t.guideConfigs.axis?.[n]?.labels?.position!==i.position)throw new Error("Label-relative title offset requires labels on the same axis side.");const c=(f?.items??(f?[f]:[])).filter(l=>it(l.properties)).map(l=>C(t.graphicSpec,l.id,t.materializationConfigs.textMetrics));if(c.length===0)throw new Error("Label-relative title offset requires nonempty axis labels.");const r=e==="x"?"top":"left",d=e==="x"?"bottom":"right",u=i.position===(e==="x"?"bottom":"right"),p=w(S({bounds:a,channel:e,position:i.position,along:s,offset:0}),i,t.semanticSpec.guides.axis?.[n]?.title??"",t.materializationConfigs.textMetrics),x=u?Math.max(...c.map(l=>l[d]))+i.offset.gap-p[r]:p[d]-Math.min(...c.map(l=>l[r]))+i.offset.gap;if(!Number.isFinite(x))throw new RangeError("Label-relative title offset must resolve to a finite distance.");return x}const X=["target","text","scale","position","at","offset","rotation","color","fontSize","fontFamily","fontWeight","alongOffset","align","baseline","overlap"],Ot=X.filter(t=>t!=="scale"),A={color:ut.text,fontSize:ct,fontFamily:pt,fontWeight:600};function O(t){return rt(t,"Axis title text")}function Y(t,e){if(bt(t,e.position),!["start","center","end"].includes(e.at)&&!Number.isFinite(e.at))throw new TypeError("Axis title at must be start, center, end, or a finite number.");if(g(e,{title:!0}),!Number.isFinite(e.rotation))throw new TypeError("Axis title rotation must be finite radians.");wt(e,"Axis title")}function U(t,e,i){const n=new Set,a=new Set;for(const s of t.semanticSpec.layers){if(st(s)||s.derivedBindings?.overlay||t.markConfigs[s.id]?.statisticalReference!==void 0)continue;const f=s.encoding?.[e],o=s.mark?.type==="area"&&Object.hasOwn(f??{},"datum")?s.encoding?.[`${e}2`]:f;if(e==="radius"&&s.mark?.type==="arc"&&o?.scale===i&&o.aggregate==="count"&&o.field===void 0){n.add(o.title??"count");continue}if(o?.scale===i&&typeof o.field=="string"&&o.field.length){const c=mt(t,s.data),r=xt(t,c,["density","ecdf","interval","boxSummary","gradientProfile","summary"]);let d;if(r?.type==="density")d=o.field===r.as?.[0]?r.field:o.field===r.as?.[1]?"Density":void 0;else if(r?.type==="ecdf")d=o.field===r.as.value?r.field:o.field===r.as.probability?"Cumulative probability":o.field===r.as.cumulative?"Cumulative count":void 0;else if(r?.type==="interval"&&Object.values(r.as).includes(o.field))d=`${r.center}(${r.field})`;else if(r?.type==="boxSummary"&&Object.values(r.as).includes(o.field))d=r.field;else if(r?.type==="gradientProfile"&&[r.as.lower,r.as.upper,r.as.center].includes(o.field))d=r.field;else if(r?.type==="summary"){const p=r.aggregates.find(x=>x.as===o.field);p&&(d=q(p.op,p.field))}const u=o.title??d??(o.aggregate===void 0?o.field:q(o.aggregate,o.field));n.add(u),s.mark?.type==="point"&&a.add(u)}}if(a.size===1)return[...a][0];if(n.size===0&&at(t,i).some(s=>s.channel===e))return i;if(n.size>1){const s=t.semanticSpec.layers.filter(o=>o.encoding?.[e]?.scale===i).map(o=>z(t,o.encoding[e],o.data)).filter(Boolean),f=new Set(s.map(o=>JSON.stringify([o.field??o.fields,o.measurement.unit])));if(s.length&&f.size===1)return s[0].field??(s[0].measurement.unit==="dimensions"?"Dimension":"Value");if(s.length&&s.every(o=>o.field&&o.measurement.unit==="value")&&!t.semanticSpec.layers.some(o=>o.encoding?.[e]?.scale===i&&o.encoding[e].title!==void 0&&o.encoding[e].title!==z(t,o.encoding[e],o.data)?.field))return[...new Set(s.map(o=>o.field))].join(" / ")}if(n.size!==1)throw new Error(`Axis title text cannot be inferred for scale "${i}".`);return[...n][0]}function V(t,e,i,n=e){const a=t.resolvedScales[i.scale],s=B(t,e,i.position,i.scale,n);if(!["linear","time","ordinal","band","point"].includes(a?.type)&&!lt(a?.type)||!s)throw new Error("Axis title requires a supported resolved scale and Canvas bounds.");const f=W(a,i),o=S({bounds:s,channel:e,position:i.position,along:f,offset:F(i.offset)?At(t,e,i,n,s,f):i.offset}),c=w(o,i,t.semanticSpec.guides.axis?.[n]?.title??"",t.materializationConfigs.textMetrics),r=P(t)?.properties;if(!r||!St(c,r))throw F(i.offset)?j(`The ${e}-axis label-relative title does not fit the Canvas margin.`,[c],r):j(`The ${e}-axis title does not fit the Canvas margin.`,[c],r);const d=k(t,e,n);if(!et(i)&&d.some(u=>u&&_(c,u)))throw new Error(`The ${e}-axis title overlaps the axis labels.`);return o}function H(t){const e=t==="x"?"X":"Y";return{create:`create${e}AxisTitle`,edit:`edit${e}AxisTitle`}}function J(t){const e=H(t);return L({op:e.edit,description:`Edit the ${t}-axis title.`},$(function(i={}){R(i,Ot,e.edit);const n=E(this,t,i,{create:!1}),a=n.id;i=n.options;const s=h(t,a)[3];if(g(i,{title:!0}),this.graphicSpec.objects[s]?.type!=="text")throw new Error(`${e.edit} requires an existing axis title.`);const f=this.guideConfigs.axis?.[a]?.title;if(!f)throw new Error(`${e.edit} requires title configuration.`);const{text:o,rotation:c,...r}=i,d=Object.hasOwn(i,"text"),u=Object.hasOwn(i,"rotation"),p=r.position??f.position,x=u?!1:f.inferredRotation===!0,l={...f,...r,position:p,rotation:x?G(t,p):u?N(c,"Axis title rotation"):f.rotation,inferredRotation:x,inferredOffset:Object.hasOwn(i,"offset")?!1:f.inferredOffset===!0,...d?{inferredText:!1}:{}};Y(t,l);let m=this;const y=d?O(o):l.inferredText===!0?U(this,t,l.scale):this.semanticSpec.guides.axis?.[a]?.title;y!==this.semanticSpec.guides.axis?.[a]?.title&&(m=m.editSemantic({property:`guide.axis.${a}.title`,value:O(y)}));const b=O(m.semanticSpec.guides.axis?.[a]?.title);l.inferredOffset&&(l.offset=D(m,t,l,a));const v=V(m,t,l,a);m=m._withGuideConfig(a,"title",l);const T={x:v.x,y:v.y,text:b,fill:l.color,fontSize:l.fontSize,fontFamily:l.fontFamily,fontWeight:l.fontWeight,textAlign:l.align??"center",textBaseline:l.baseline??"middle",rotation:l.rotation};for(const[Z,Q]of Object.entries(T))m=m.editGraphics({target:s,property:Z,value:Q});return tt(m,s,M(b,l))}))}const gt=J("x"),Ct=J("y");function K(t){const e=H(t);return L({op:e.create,description:`Create the ${t}-axis title.`},$(function(i={}){R(i,X,e.create);const n=E(this,t,i,{create:!0}),a=n.id;i=n.options;const s=h(t,a)[3];g(i,{title:!0});const{text:f,scale:o,position:c,rotation:r,...d}=i,u=nt(o??t,"Scale id"),p=this.semanticSpec.guides.axis?.[a]?.scale;if(p&&p!==u)throw new Error(`${e.create} conflicts with the existing axis scale.`);if(this.graphicSpec.objects[s])throw new Error(`${e.create} requires a missing axis title.`);const x=!Object.hasOwn(i,"text"),l=O(f??U(this,t,u)),m=c??yt(t),y=!Object.hasOwn(i,"rotation"),b=!Object.hasOwn(i,"offset"),v={scale:u,inferredText:x,position:m,at:"center",offset:t==="x"?42:52,rotation:y?G(t,m):N(r,"Axis title rotation"),inferredRotation:y,inferredOffset:b,color:A.color,fontSize:A.fontSize,fontFamily:A.fontFamily,fontWeight:A.fontWeight,...ot(this.materializationConfigs.theme,"title"),...d};let T=this.editSemantic({property:`guide.axis.${a}.scale`,value:u}).editSemantic({property:`guide.axis.${a}.title`,value:l});return b&&(v.offset=D(T,t,v,a)),Y(t,v),V(T,t,v,a),T.createGraphics({id:s,type:"text",...Tt(this)})._withGuideConfig(a,"title",v)[e.edit](a===t?void 0:I(a,t))}))}const zt=K("x"),Et=K("y");function te(t){t.prototype.createXAxisTitle=zt,t.prototype.createYAxisTitle=Et,t.prototype.editXAxisTitle=gt,t.prototype.editYAxisTitle=Ct}export{U as inferAxisTitleText,te as registerAxisTitleActions};
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import {colorSizeScaleIds} from "../../grammar/colorSizeLegend.js";
|
|
1
2
|
import { resolveLegendCreationPlan } from "./legends/categorical/actions.js";
|
|
2
3
|
import { resolveLegendStepConfig } from "./legends/creation.js";
|
|
3
4
|
import { legendResourcePolicy } from "../../materialization/guides/resources.js";
|
|
4
5
|
import { assertGuideOptions, guideConflict, sameGuideValue } from "./reuse.js";
|
|
5
6
|
|
|
6
7
|
function compatibleLegend(program, kind, requested, existing, explicit) {
|
|
7
|
-
const expectedScales = kind === "colorSize" ?
|
|
8
|
-
const actualScales = kind === "colorSize" ?
|
|
8
|
+
const expectedScales = kind === "colorSize" ? colorSizeScaleIds(requested) : requested.scales ?? [requested.scale];
|
|
9
|
+
const actualScales = kind === "colorSize" ? colorSizeScaleIds(existing) : existing.scales ?? [existing.scale];
|
|
9
10
|
if (!sameGuideValue(expectedScales, actualScales) ||
|
|
10
11
|
!sameGuideValue(requested.channels, existing.channels)) {
|
|
11
12
|
guideConflict(`${kind} legend uses different channels or scales`);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withGuideVisibilityPaint } from "../../../materialization/guides/visibility.js";
|
|
1
2
|
import { editGraphicProperties } from "../../primitives/graphicProperties.js";
|
|
2
3
|
import { action, closedAction } from "../../../core/action.js";
|
|
3
4
|
import { isPlainObject } from "../../../core/immutable.js";
|
|
@@ -45,7 +46,7 @@ function makeRematerialize(direction) {
|
|
|
45
46
|
op: operation.rematerialize,
|
|
46
47
|
description: `Recompute concrete ${direction} grid lines.`
|
|
47
48
|
},
|
|
48
|
-
function (args = {}) {
|
|
49
|
+
withGuideVisibilityPaint(function (args = {}) {
|
|
49
50
|
noOptions(args, operation.rematerialize);
|
|
50
51
|
const storedConfig = this.guideConfigs.grid?.[direction];
|
|
51
52
|
const semantic = this.semanticSpec.guides.grid?.[direction];
|
|
@@ -89,7 +90,7 @@ function makeRematerialize(direction) {
|
|
|
89
90
|
value: geometry.strokeDash
|
|
90
91
|
});
|
|
91
92
|
return hasPlotUnderlays(next) ? next.rematerializePlotDrawOrder() : next;
|
|
92
|
-
}
|
|
93
|
+
})
|
|
93
94
|
);
|
|
94
95
|
}
|
|
95
96
|
|
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
const GRID_OPTIONS = Object.freeze([
|
|
35
35
|
"scale", "coordinate", "count", "values", "color", "lineWidth", "strokeDash", "strokeDashMap", "zIndex", "bandPosition", "alongOffset"
|
|
36
36
|
]);
|
|
37
|
-
const GRID_EDIT_OPTIONS = Object.freeze([
|
|
37
|
+
const GRID_EDIT_OPTIONS = Object.freeze(["visible",
|
|
38
38
|
"count", "values", "color", "lineWidth", "strokeDash", "strokeDashMap", "zIndex", "bandPosition", "alongOffset"
|
|
39
39
|
]);
|
|
40
40
|
const DEFAULT_STYLE = Object.freeze({
|
|
@@ -82,6 +82,7 @@ export function validateGridCreateArgs(args, operation) {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
export function validateGridEditArgs(args, operation) {
|
|
85
|
+
if (Object.hasOwn(args, "visible") && typeof args.visible !== "boolean") throw new TypeError("Grid visible must be a boolean.");
|
|
85
86
|
if (Object.hasOwn(args, "strokeDashMap")) normalizeGuideDashMap(args.strokeDashMap);
|
|
86
87
|
validateOptionObject(args, GRID_EDIT_OPTIONS, operation, {
|
|
87
88
|
allowEmpty: false
|
|
@@ -9,6 +9,7 @@ import { findDataset } from "../../selectors/datasets.js";
|
|
|
9
9
|
import { findUpstreamTransform } from
|
|
10
10
|
"../../materialization/dataProvenance.js";
|
|
11
11
|
import { sampleNumericRange } from "../../grammar/numeric.js";
|
|
12
|
+
import { automaticLegendOptions } from "./legends/automatic.js";
|
|
12
13
|
|
|
13
14
|
const OPTIONS = Object.freeze(["axes", "grid", "legend"]);
|
|
14
15
|
|
|
@@ -121,10 +122,10 @@ export function resolveGuideOptions(program, args = {}, layers = program.semanti
|
|
|
121
122
|
args.grid,
|
|
122
123
|
applicability.grid.cartesian || applicability.grid.polar
|
|
123
124
|
);
|
|
124
|
-
const legend = selectOption(
|
|
125
|
+
const legend = automaticLegendOptions(program, selectOption(
|
|
125
126
|
args.legend,
|
|
126
127
|
applicability.legend
|
|
127
|
-
);
|
|
128
|
+
), layers);
|
|
128
129
|
return { axes, grid, legend };
|
|
129
130
|
}
|
|
130
131
|
|