ggaction 0.0.17 → 0.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +96 -0
- package/README.md +6 -2
- package/knowledge/action-card.schema.json +1 -1
- 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 +3 -3
- package/src/BasicChartProgram.js +1 -1
- package/src/accessibility.js +3 -2
- package/src/actions/boxPlots/create.js +14 -4
- package/src/actions/boxPlots/edit.js +39 -33
- package/src/actions/boxPlots/materialize.js +7 -3
- package/src/actions/boxPlots/options.js +37 -11
- package/src/actions/canvas/actions.js +70 -95
- package/src/actions/canvas/fitting.js +20 -1
- package/src/actions/canvas/index.js +3 -0
- package/src/actions/canvas/plotBorder.js +28 -0
- package/src/actions/charts/area.js +2 -2
- package/src/actions/charts/bar.js +34 -7
- package/src/actions/charts/ecdf.js +4 -18
- package/src/actions/charts/endpoints.js +4 -18
- package/src/actions/charts/heatmap.js +8 -7
- package/src/actions/charts/index.js +2 -0
- package/src/actions/charts/interval-regression.js +2 -2
- package/src/actions/charts/line.js +1 -1
- package/src/actions/charts/pie.js +3 -47
- package/src/actions/charts/radial.js +34 -12
- package/src/actions/charts/scatter.js +16 -2
- package/src/actions/charts/shared.js +2 -2
- package/src/actions/charts/text.js +35 -0
- package/src/actions/composition/actions.js +6 -2
- package/src/actions/data/box.js +2 -1
- package/src/actions/data/derived.js +11 -1
- package/src/actions/data/edit.js +3 -1
- package/src/actions/data/index.js +4 -0
- package/src/actions/data/regression.js +25 -16
- package/src/actions/data/regressionStatistics.js +73 -0
- package/src/actions/data/shared.js +8 -4
- package/src/actions/data/stack.js +3 -1
- package/src/actions/distributions/revision.js +24 -18
- package/src/actions/encodings/appearance.js +37 -29
- package/src/actions/encodings/barWidth.js +36 -11
- package/src/actions/encodings/channels.js +0 -1
- package/src/actions/encodings/color/continuous.js +1 -1
- package/src/actions/encodings/color/index.js +1 -1
- package/src/actions/encodings/color/policy.js +10 -3
- package/src/actions/encodings/density/resolve.js +1 -6
- package/src/actions/encodings/position/policies/arc.js +12 -10
- package/src/actions/encodings/position/policies/area.js +3 -3
- package/src/actions/encodings/position/policies/bar.js +15 -6
- package/src/actions/encodings/position/policies/line.js +17 -6
- package/src/actions/encodings/position/resolve.js +3 -2
- package/src/actions/encodings/ranged.js +17 -0
- package/src/actions/encodings/remove.js +14 -8
- package/src/actions/encodings/seriesLayout.js +1 -0
- package/src/actions/encodings/shared.js +26 -24
- package/src/actions/encodings/text.js +6 -3
- package/src/actions/errorBands/create.js +2 -2
- package/src/actions/errorBands/edit.js +2 -1
- package/src/actions/facets/actions.js +176 -247
- package/src/actions/facets/guides.js +2 -1
- package/src/actions/gradientPlots/resolve.js +2 -17
- package/src/actions/guides/applicability.js +1 -1
- package/src/actions/guides/axes/axes.js +30 -11
- package/src/actions/guides/axes/axis.js +72 -10
- package/src/actions/guides/axes/components.js +13 -13
- package/src/actions/guides/axes/edit.js +36 -24
- package/src/actions/guides/axes/labels.js +155 -71
- package/src/actions/guides/axes/lines.js +27 -24
- package/src/actions/guides/axes/parallel/lifecycle.js +1 -1
- package/src/actions/guides/axes/parallel/policy.js +8 -7
- package/src/actions/guides/axes/remove.js +13 -8
- package/src/actions/guides/axes/tickGroups.js +11 -7
- package/src/actions/guides/axes/ticks.js +36 -24
- package/src/actions/guides/axes/titles.js +107 -69
- package/src/actions/guides/basic.js +2 -0
- package/src/actions/guides/facadeAxes.js +33 -24
- package/src/actions/guides/facadeLegend.js +12 -4
- package/src/actions/guides/grids/grid.js +8 -2
- package/src/actions/guides/grids/resolve.js +102 -58
- package/src/actions/guides/legends/blocks.js +46 -20
- package/src/actions/guides/legends/categorical/actions.js +95 -19
- package/src/actions/guides/legends/categorical/components.js +15 -18
- package/src/actions/guides/legends/categorical/index.js +8 -0
- package/src/actions/guides/legends/categorical/layout.js +45 -20
- package/src/actions/guides/legends/categorical/options.js +39 -22
- package/src/actions/guides/legends/categorical/recipes.js +7 -3
- package/src/actions/guides/legends/categorical/resolve.js +11 -4
- package/src/actions/guides/legends/categorical/symbols.js +38 -18
- package/src/actions/guides/legends/categorical/validation.js +1 -6
- package/src/actions/guides/legends/colorSize.js +110 -0
- package/src/actions/guides/legends/combine.js +37 -0
- package/src/actions/guides/legends/continuous/colorSymbols.js +1 -0
- package/src/actions/guides/legends/continuous/common.js +1 -447
- package/src/actions/guides/legends/continuous/gradient.js +73 -46
- package/src/actions/guides/legends/continuous/interval.js +4 -4
- package/src/actions/guides/legends/continuous/opacity.js +33 -21
- package/src/actions/guides/legends/creation.js +2 -0
- package/src/actions/guides/legends/edit.js +1 -604
- package/src/actions/guides/legends/focused.js +8 -7
- package/src/actions/guides/legends/group.js +1 -0
- package/src/actions/guides/legends/lane.js +1 -516
- package/src/actions/guides/legends/lifecycle.js +2 -1
- package/src/actions/guides/legends/remove.js +9 -3
- package/src/actions/guides/legends/sampling.js +1 -1
- package/src/actions/guides/legends/size.js +65 -47
- package/src/actions/guides/legends/strokeWidth.js +13 -29
- package/src/actions/guides/legends/target.js +2 -79
- package/src/actions/guides/legends/transition.js +44 -11
- package/src/actions/guides/polar/axes/labels.js +4 -2
- package/src/actions/guides/polar/axes/titles.js +7 -5
- package/src/actions/guides/polar/resolve.js +6 -14
- package/src/actions/guides/tickValues.js +8 -5
- package/src/actions/index.js +16 -16
- package/src/actions/marks/arc/actions.js +3 -10
- package/src/actions/marks/area/actions.js +8 -5
- package/src/actions/marks/area/materialize.js +8 -9
- package/src/actions/marks/bar/create.js +10 -4
- package/src/actions/marks/bar/edit.js +29 -14
- package/src/actions/marks/bar/materialize.js +15 -7
- package/src/actions/marks/line/actions.js +11 -4
- package/src/actions/marks/line/materialize.js +9 -5
- package/src/actions/marks/point/edit.js +1 -1
- package/src/actions/marks/point/materialize.js +5 -1
- package/src/actions/marks/rect/actions.js +3 -3
- package/src/actions/marks/remove.js +12 -10
- package/src/actions/marks/rule/actions.js +7 -4
- package/src/actions/marks/text/actions.js +17 -5
- package/src/actions/marks/text/layout.js +3 -0
- package/src/actions/primitives/createGraphics.js +3 -2
- package/src/actions/primitives/editGraphics.js +15 -11
- package/src/actions/primitives/graphicProperties.js +1 -6
- package/src/actions/primitives/semanticAction.js +27 -1
- package/src/actions/primitives/semanticValidation/guide.js +11 -1
- package/src/actions/primitives/semanticValidation/layer.js +8 -0
- package/src/actions/regression/create.js +1 -1
- package/src/actions/regression/edit.js +18 -6
- package/src/actions/regression/resolve.js +2 -6
- package/src/actions/scales/channels.js +9 -4
- package/src/actions/scales/consumers/families.js +16 -5
- package/src/actions/scales/create.js +10 -5
- package/src/actions/scales/definitions.js +1 -425
- package/src/actions/scales/edit.js +3 -3
- package/src/actions/scales/editPolicy.js +12 -3
- package/src/actions/scales/materialize.js +1 -1
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/preview.js +21 -1
- package/src/actions/scales/quantitativeColor.js +8 -3
- package/src/actions/selection/actions.js +1 -13
- package/src/actions/theme/reconcile.js +126 -45
- package/src/actions/theme/state.js +12 -0
- package/src/actions/titles/actions.js +5 -18
- package/src/actions/titles/resolve.js +41 -19
- package/src/actions/violinPlots/edit.js +3 -20
- package/src/core/compositionState.js +6 -3
- package/src/core/font.js +9 -2
- package/src/core/graphicProperties.js +1 -0
- package/src/core/optionRequirements.js +1 -0
- package/src/core/programState.js +4 -0
- package/src/core/resourceReferences.js +5 -2
- package/src/core/textMetricProfile.js +3 -2
- package/src/core/textMetrics.js +41 -4
- package/src/core/validation.js +35 -0
- package/src/core/vocabulary.js +7 -0
- package/src/grammar/aggregate.js +4 -18
- package/src/grammar/arcs.js +1 -386
- package/src/grammar/areaPaint.js +10 -0
- package/src/grammar/areaSeries.js +18 -10
- package/src/grammar/axisTextPlacement.js +1 -0
- package/src/grammar/bars/geometry.js +30 -3
- package/src/grammar/bars/policy.js +12 -1
- package/src/grammar/bin.js +2 -12
- package/src/grammar/bin2d.js +1 -6
- package/src/grammar/boxPlot.js +56 -9
- package/src/grammar/categoryOrder.js +1 -6
- package/src/grammar/colorSizeLegend.js +39 -0
- package/src/grammar/complete.js +15 -19
- package/src/grammar/computed.js +115 -26
- package/src/grammar/curveCommands.js +29 -16
- package/src/grammar/datasetSchema.js +13 -9
- package/src/grammar/displayLabels.js +36 -0
- package/src/grammar/ecdf.js +2 -9
- package/src/grammar/facets/dependencies.js +6 -2
- package/src/grammar/facets/guides.js +58 -30
- package/src/grammar/facets/index.js +2 -5
- package/src/grammar/facets/legendIntent.js +65 -0
- package/src/grammar/fold.js +2 -12
- package/src/grammar/gradientProfile.js +1 -6
- package/src/grammar/guideDash.js +1 -0
- package/src/grammar/impute.js +21 -48
- package/src/grammar/interval.js +2 -9
- package/src/grammar/legendBlocks.js +1 -0
- package/src/grammar/legendBorder.js +9 -0
- package/src/grammar/legendClip.js +19 -0
- package/src/grammar/legendCombinations.js +45 -0
- package/src/grammar/legendGradient.js +27 -0
- package/src/grammar/legendGroups.js +1 -0
- package/src/grammar/legendOpacity.js +6 -0
- package/src/grammar/legendTitleStyle.js +22 -0
- package/src/grammar/lineSeries.js +32 -17
- package/src/grammar/markLabels.js +5 -2
- package/src/grammar/normalize.js +4 -54
- package/src/grammar/numeric.js +1 -290
- package/src/grammar/parallelCoordinates.js +1 -6
- package/src/grammar/parseTime.js +41 -0
- package/src/grammar/pathCommands.js +11 -4
- package/src/grammar/pointShapes.js +16 -2
- package/src/grammar/polar.js +1 -184
- package/src/grammar/positionCompatibility.js +3 -3
- package/src/grammar/regression/derive.js +16 -2
- package/src/grammar/regression/models.js +142 -54
- package/src/grammar/regression/parameters.js +23 -4
- package/src/grammar/regression/statistics.js +64 -0
- package/src/grammar/roundedRect.js +42 -54
- package/src/grammar/scales/appearance.js +1 -206
- package/src/grammar/scales/color.js +1 -382
- package/src/grammar/scales/colorConsumers.js +5 -2
- package/src/grammar/scales/definition.js +1 -197
- package/src/grammar/scales/discretized.js +1 -195
- package/src/grammar/scales/size.js +1 -456
- package/src/grammar/scales/temporal.js +2 -13
- package/src/grammar/scales/transformed.js +1 -383
- package/src/grammar/scales/types.js +1 -1
- package/src/grammar/scales/validation.js +1 -66
- package/src/grammar/schemas/concreteGraphic.js +14 -0
- package/src/grammar/schemas/graphic.js +6 -0
- package/src/grammar/schemas/graphicBounds.js +31 -7
- package/src/grammar/schemas/semanticPath.js +19 -3
- package/src/grammar/stack.js +2 -10
- package/src/grammar/statisticalReference.js +1 -6
- package/src/grammar/summary.js +4 -27
- package/src/grammar/text.js +19 -0
- package/src/grammar/timeUnit.js +22 -11
- package/src/grammar/transformKeys.js +47 -0
- package/src/grammar/transformTopology.js +1 -0
- package/src/grammar/transforms.js +15 -2
- package/src/grammar/valueFormat.js +17 -5
- package/src/grammar/weightedStatistics.js +1 -6
- package/src/grammar/window.js +4 -58
- package/src/inspectionDescriptors.js +1 -1
- package/src/layout/canvas.js +1 -110
- package/src/layout/facets.js +1 -244
- package/src/layout/guideCollisions.js +9 -1
- package/src/layout/labels.js +1 -0
- package/src/layout/legend.js +13 -0
- package/src/layout/legendGroup.js +1 -0
- package/src/layout/legendItems.js +59 -15
- package/src/layout/legendLane.js +1 -441
- package/src/layout/legendText.js +1 -0
- package/src/layout/title.js +35 -13
- package/src/materialization/bars/aggregate.js +11 -5
- package/src/materialization/bars/histogram.js +4 -1
- package/src/materialization/bars/ranged.js +19 -6
- package/src/materialization/bars/resolve.js +22 -1
- package/src/materialization/compositionSnapshot.js +2 -7
- package/src/materialization/coordinateBounds.js +21 -0
- package/src/materialization/dependencies.js +4 -44
- package/src/materialization/facetGuides/edit.js +30 -0
- package/src/materialization/facetGuides/layout.js +1 -0
- package/src/materialization/facetGuides/owner.js +125 -0
- package/src/materialization/facetGuides/ownerLayout.js +71 -0
- package/src/materialization/facetGuides/placement.js +71 -14
- package/src/materialization/facetGuides/preparation.js +22 -7
- package/src/materialization/facetHeaders.js +1 -347
- package/src/materialization/facets.js +1 -185
- package/src/materialization/graphicHierarchy.js +5 -1
- package/src/materialization/graphicTranslation.js +73 -0
- package/src/materialization/guides/layout.js +1 -57
- package/src/materialization/guides/resources.js +1 -123
- package/src/materialization/layout.js +53 -0
- package/src/materialization/legendBorder.js +28 -0
- package/src/materialization/legendClip.js +76 -0
- package/src/materialization/legendGroups.js +1 -0
- package/src/materialization/legendText.js +1 -0
- package/src/materialization/legends.js +2 -2
- package/src/materialization/marks/capabilities.js +5 -6
- package/src/materialization/rect.js +1 -1
- package/src/materialization/scaleGuideDependencies.js +14 -15
- package/src/materialization/scales/map.js +6 -0
- package/src/materialization/scales/policies/arc.js +8 -7
- package/src/materialization/scales/policies/bar.js +19 -7
- package/src/materialization/scales/policies/series.js +2 -1
- package/src/materialization/scales/resolve.js +7 -3
- package/src/materialization/selection/items/arc.js +1 -1
- package/src/materialization/selection/items/bar.js +7 -6
- package/src/materialization/selection/items/path.js +7 -6
- package/src/materialization/selection/items/rect.js +2 -2
- package/src/materialization/text.js +13 -4
- package/src/materialization/textLines.js +15 -0
- package/src/materialization/typography.js +10 -2
- package/src/persistence/codec.js +37 -6
- package/src/persistence/validation.js +220 -0
- package/src/persistence.js +19 -6
- package/src/renderers/canvas/circle.js +6 -3
- package/src/renderers/canvas/fill.js +7 -1
- package/src/renderers/canvas/index.js +22 -11
- package/src/renderers/canvas/isolated.js +166 -0
- package/src/renderers/canvas/limits.js +3 -0
- package/src/renderers/canvas/line.js +1 -0
- package/src/renderers/canvas/path.js +1 -1
- package/src/renderers/canvas/rect.js +11 -8
- package/src/renderers/canvas/stroke.js +2 -1
- package/src/renderers/canvas/text.js +5 -3
- package/src/renderers/pdf.js +2 -2
- package/src/renderers/png.js +2 -2
- package/src/renderers/svg.js +14 -4
- package/src/selectors/axes.js +1 -0
- package/src/selectors/graphicCapacity.js +38 -0
- package/src/selectors/layers.js +19 -0
- package/src/theme/defaults.js +8 -2
- package/src/version.js +1 -1
- package/types/basic.d.ts +3 -0
- package/types/index.d.ts +12 -0
- package/types/program.d.ts +536 -129
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { selectCartesianAxis } from "../../../selectors/axes.js";
|
|
2
|
+
import { withGuideLayoutValidation } from "../../../materialization/guides/layout.js";
|
|
1
3
|
import { action } from "../../../core/action.js";
|
|
2
4
|
import { validateKeys } from "../../../core/validation.js";
|
|
3
5
|
import { axisGraphicIds } from
|
|
@@ -14,16 +16,19 @@ function makeRemoveAxis(channel) {
|
|
|
14
16
|
? "Theta"
|
|
15
17
|
: "Radial";
|
|
16
18
|
const operation = `remove${prefix}Axis`;
|
|
17
|
-
const graphicIds = axisGraphicIds(channel);
|
|
18
19
|
return action(
|
|
19
20
|
{
|
|
20
21
|
op: operation,
|
|
21
22
|
description: `Remove the complete ${channel}-axis resource.`
|
|
22
23
|
},
|
|
23
|
-
function (args = {}) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
24
|
+
withGuideLayoutValidation(function (args = {}) {
|
|
25
|
+
const cartesian = ["x", "y"].includes(channel);
|
|
26
|
+
validateKeys(args, cartesian ? [...OPTIONS, "target"] : OPTIONS, operation);
|
|
27
|
+
const selected = cartesian ? selectCartesianAxis(this, channel, args, { whole: true }) : { id: channel, options: args };
|
|
28
|
+
const axisId = selected.id; args = selected.options;
|
|
29
|
+
const graphicIds = axisGraphicIds(channel, axisId);
|
|
30
|
+
const semantic = this.semanticSpec.guides.axis?.[axisId];
|
|
31
|
+
const config = this.guideConfigs.axis?.[axisId];
|
|
27
32
|
const hasGraphic = graphicIds.some(
|
|
28
33
|
id => this.graphicSpec.objects[id] !== undefined
|
|
29
34
|
);
|
|
@@ -41,7 +46,7 @@ function makeRemoveAxis(channel) {
|
|
|
41
46
|
let next = semantic === undefined
|
|
42
47
|
? this
|
|
43
48
|
: this.editSemantic({
|
|
44
|
-
property: `guide.axis.${
|
|
49
|
+
property: `guide.axis.${axisId}`,
|
|
45
50
|
remove: true
|
|
46
51
|
});
|
|
47
52
|
for (const id of graphicIds) {
|
|
@@ -49,8 +54,8 @@ function makeRemoveAxis(channel) {
|
|
|
49
54
|
next = next.editGraphics({ target: id, remove: true });
|
|
50
55
|
}
|
|
51
56
|
}
|
|
52
|
-
return next._withoutMaterializationConfig(["guides", "axis",
|
|
53
|
-
}
|
|
57
|
+
return next._withoutMaterializationConfig(["guides", "axis", axisId]);
|
|
58
|
+
})
|
|
54
59
|
);
|
|
55
60
|
}
|
|
56
61
|
|
|
@@ -3,7 +3,7 @@ import { action } from "../../../core/action.js";
|
|
|
3
3
|
import { isPlainObject } from "../../../core/immutable.js";
|
|
4
4
|
import { validateKeys, validateOptionObject } from "../../../core/validation.js";
|
|
5
5
|
|
|
6
|
-
const CREATE_OPTIONS = Object.freeze([
|
|
6
|
+
const CREATE_OPTIONS = Object.freeze(["target",
|
|
7
7
|
"scale",
|
|
8
8
|
"position",
|
|
9
9
|
"count",
|
|
@@ -11,14 +11,14 @@ const CREATE_OPTIONS = Object.freeze([
|
|
|
11
11
|
"ticks",
|
|
12
12
|
"labels"
|
|
13
13
|
]);
|
|
14
|
-
const EDIT_OPTIONS = Object.freeze([
|
|
14
|
+
const EDIT_OPTIONS = Object.freeze(["target",
|
|
15
15
|
"position",
|
|
16
16
|
"count",
|
|
17
17
|
"values",
|
|
18
18
|
"ticks",
|
|
19
19
|
"labels"
|
|
20
20
|
]);
|
|
21
|
-
const TICK_OPTIONS = Object.freeze(["length", "color", "lineWidth"]);
|
|
21
|
+
const TICK_OPTIONS = Object.freeze(["length", "color", "lineWidth", "alongOffset", "strokeDash", "strokeDashMap"]);
|
|
22
22
|
const LABEL_OPTIONS = Object.freeze([
|
|
23
23
|
"offset",
|
|
24
24
|
"format",
|
|
@@ -31,10 +31,14 @@ const LABEL_OPTIONS = Object.freeze([
|
|
|
31
31
|
"wrap",
|
|
32
32
|
"lineHeight",
|
|
33
33
|
"overlap",
|
|
34
|
-
"labelMap"
|
|
34
|
+
"labelMap",
|
|
35
|
+
"alongOffset",
|
|
36
|
+
"flush",
|
|
37
|
+
"colorMap",
|
|
38
|
+
"align"
|
|
35
39
|
]);
|
|
36
|
-
const SHARED_CREATE = Object.freeze(["scale", "position", "count", "values"]);
|
|
37
|
-
const SHARED_EDIT = Object.freeze(["position", "count", "values"]);
|
|
40
|
+
const SHARED_CREATE = Object.freeze(["target", "scale", "position", "count", "values"]);
|
|
41
|
+
const SHARED_EDIT = Object.freeze(["target", "position", "count", "values"]);
|
|
38
42
|
|
|
39
43
|
function validateNested(value, supported, label) {
|
|
40
44
|
if (!isPlainObject(value)) {
|
|
@@ -101,7 +105,7 @@ function makeCreate(channel) {
|
|
|
101
105
|
const shared = select(args, SHARED_CREATE);
|
|
102
106
|
const tickArgs = { ...shared, ...(args.ticks ?? {}) };
|
|
103
107
|
const labelArgs = {
|
|
104
|
-
...select(shared, ["scale", "position"]),
|
|
108
|
+
...select(shared, ["target", "scale", "position"]),
|
|
105
109
|
...(args.labels ?? {})
|
|
106
110
|
};
|
|
107
111
|
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { selectCartesianAxis, axisTargetArgs } from "../../../selectors/axes.js";
|
|
2
|
+
import { axisGraphicIds } from "../../../materialization/guides/resources.js";
|
|
3
|
+
import { canvasOverflowError } from "../../../layout/canvas.js";
|
|
4
|
+
import { patchGuideDash, validateGuideDash, normalizeGuideDashMap, resolveGuideDashes } from "../../../grammar/guideDash.js";
|
|
1
5
|
import { editGraphicProperties } from "../../primitives/graphicProperties.js";
|
|
2
6
|
import { withGuideLayoutValidation } from "../../../materialization/guides/layout.js";
|
|
3
7
|
import { applyMaterializationPlan } from "../../../materialization/planner.js";
|
|
@@ -10,7 +14,7 @@ import {
|
|
|
10
14
|
validateNonNegativeFinite,
|
|
11
15
|
validateOptionObject
|
|
12
16
|
} from "../../../core/validation.js";
|
|
13
|
-
import {
|
|
17
|
+
import { resolveCartesianAxisFrameBounds } from "../../../materialization/coordinateBounds.js";
|
|
14
18
|
import {
|
|
15
19
|
isTransformedScaleType,
|
|
16
20
|
mapContinuousScaleValues,
|
|
@@ -31,7 +35,7 @@ import {
|
|
|
31
35
|
import { findCanvasGraphic, resolvePlotGraphicPlacement } from
|
|
32
36
|
"../../../materialization/graphicHierarchy.js";
|
|
33
37
|
|
|
34
|
-
const OPTIONS = Object.freeze(["scale", "position", "count", "values", "length", "color", "lineWidth"]);
|
|
38
|
+
const OPTIONS = Object.freeze(["target", "scale", "position", "count", "values", "length", "color", "lineWidth", "alongOffset", "strokeDash", "strokeDashMap"]);
|
|
35
39
|
const DEFAULTS = Object.freeze({
|
|
36
40
|
count: DEFAULT_TICK_COUNT,
|
|
37
41
|
length: 6,
|
|
@@ -45,6 +49,7 @@ function validateOptions(args, operation, create) {
|
|
|
45
49
|
create ? OPTIONS : OPTIONS.filter(key => key !== "scale"),
|
|
46
50
|
operation
|
|
47
51
|
);
|
|
52
|
+
if (Object.hasOwn(args, "alongOffset") && !Number.isFinite(args.alongOffset)) throw new TypeError("Tick alongOffset must be a finite number.");
|
|
48
53
|
if (Object.hasOwn(args, "count") && Object.hasOwn(args, "values")) throw new Error(`${operation} cannot use count and values together.`);
|
|
49
54
|
if (Array.isArray(args.values)) {
|
|
50
55
|
validateGeneratedItemLimit(args.values.length, "Tick value count");
|
|
@@ -52,6 +57,8 @@ function validateOptions(args, operation, create) {
|
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
function validateConfig(channel, config) {
|
|
60
|
+
if (config.strokeDash !== undefined) validateGuideDash(config.strokeDash, "Tick strokeDash");
|
|
61
|
+
if (config.strokeDashMap !== undefined) normalizeGuideDashMap(config.strokeDashMap, { reset: false });
|
|
55
62
|
validateAxisPosition(channel, config.position);
|
|
56
63
|
if (config.mode === "count") {
|
|
57
64
|
if (!Number.isInteger(config.count) || config.count <= 0) throw new RangeError("Tick count must be a positive integer.");
|
|
@@ -65,18 +72,15 @@ function validateConfig(channel, config) {
|
|
|
65
72
|
(typeof value === "number" && Number.isFinite(value))
|
|
66
73
|
))
|
|
67
74
|
) throw new TypeError("Tick values must be nominal values or finite numbers.");
|
|
75
|
+
if (config.alongOffset !== undefined && !Number.isFinite(config.alongOffset)) throw new TypeError("Tick alongOffset must be a finite number.");
|
|
68
76
|
validateNonNegativeFinite(config.length, "Tick length");
|
|
69
77
|
validateNonNegativeFinite(config.lineWidth, "Tick lineWidth");
|
|
70
78
|
validateNonEmptyString(config.color, "Tick color");
|
|
71
79
|
}
|
|
72
80
|
|
|
73
|
-
function geometry(program, channel, config) {
|
|
81
|
+
function geometry(program, channel, config, axisId = channel) {
|
|
74
82
|
const scale = program.resolvedScales[config.scale];
|
|
75
|
-
const bounds =
|
|
76
|
-
coordinate: program.semanticSpec.guides.axis?.[channel]?.coordinate,
|
|
77
|
-
scale: config.scale,
|
|
78
|
-
channel
|
|
79
|
-
});
|
|
83
|
+
const bounds = resolveCartesianAxisFrameBounds(program, channel, config.position, config.scale, axisId);
|
|
80
84
|
const discrete = ["ordinal", "band", "point"].includes(scale?.type);
|
|
81
85
|
if ((
|
|
82
86
|
!["linear", "time", "ordinal", "band", "point"].includes(scale?.type) &&
|
|
@@ -102,7 +106,7 @@ function geometry(program, channel, config) {
|
|
|
102
106
|
bounds,
|
|
103
107
|
channel,
|
|
104
108
|
position: config.position,
|
|
105
|
-
positions,
|
|
109
|
+
positions: positions.map(value => value + (config.alongOffset ?? 0)),
|
|
106
110
|
length: config.length
|
|
107
111
|
})
|
|
108
112
|
};
|
|
@@ -127,7 +131,7 @@ function geometry(program, channel, config) {
|
|
|
127
131
|
coordinates.some(value => !Number.isFinite(value)) ||
|
|
128
132
|
!fits
|
|
129
133
|
) {
|
|
130
|
-
throw
|
|
134
|
+
throw canvasOverflowError(`The ${channel}-axis ticks do not fit the Canvas margin.`, [{ left: left - strokeExtent, right: right + strokeExtent, top: top - strokeExtent, bottom: bottom + strokeExtent }], canvas);
|
|
131
135
|
}
|
|
132
136
|
return resolved;
|
|
133
137
|
}
|
|
@@ -136,9 +140,11 @@ function makeEdit(channel) {
|
|
|
136
140
|
const op = channel === "x" ? "editXAxisTicks" : "editYAxisTicks";
|
|
137
141
|
return action({ op, description: `Edit concrete ${channel}-axis ticks.` }, withGuideLayoutValidation(function (args = {}) {
|
|
138
142
|
validateOptions(args, op, false);
|
|
139
|
-
const
|
|
143
|
+
const selected = selectCartesianAxis(this, channel, args, { create: false });
|
|
144
|
+
const axisId = selected.id; args = selected.options;
|
|
145
|
+
const id = axisGraphicIds(channel, axisId)[1];
|
|
140
146
|
if (this.graphicSpec.objects[id]?.type !== "line") throw new Error(`${op} requires existing axis ticks.`);
|
|
141
|
-
const previous = this.guideConfigs.axis?.[
|
|
147
|
+
const previous = this.guideConfigs.axis?.[axisId]?.ticks;
|
|
142
148
|
if (!previous) throw new Error(`${op} requires tick configuration.`);
|
|
143
149
|
const explicitMode = Object.hasOwn(args, "values") || Object.hasOwn(args, "count");
|
|
144
150
|
const inferredValues = !explicitMode && previous.inferredValues === true
|
|
@@ -152,24 +158,28 @@ function makeEdit(channel) {
|
|
|
152
158
|
const mode = Object.hasOwn(args, "values") || inferredValues !== undefined
|
|
153
159
|
? "values"
|
|
154
160
|
: Object.hasOwn(args, "count") ? "count" : previous.mode;
|
|
155
|
-
const config = {
|
|
161
|
+
const config = patchGuideDash({
|
|
156
162
|
...previous,
|
|
157
163
|
...args,
|
|
158
164
|
...(inferredValues === undefined ? {} : { values: inferredValues }),
|
|
159
165
|
...(explicitMode ? { inferredValues: false } : {}),
|
|
160
166
|
mode
|
|
161
|
-
};
|
|
167
|
+
}, args);
|
|
162
168
|
if (mode === "values") delete config.count; else delete config.values;
|
|
163
169
|
validateConfig(channel, config);
|
|
164
|
-
const resolved = geometry(this, channel, config);
|
|
165
|
-
let next = this._withGuideConfig(
|
|
170
|
+
const resolved = geometry(this, channel, config, axisId);
|
|
171
|
+
let next = this._withGuideConfig(axisId, config).editGraphics({ target: id, property: "length", value: resolved.values.length });
|
|
166
172
|
for (const property of ["x1", "y1", "x2", "y2"]) next = next.editGraphics({ target: id, property, value: resolved[property] });
|
|
167
173
|
next = editGraphicProperties(next, id, {
|
|
168
174
|
stroke: config.color,
|
|
169
175
|
strokeWidth: config.lineWidth
|
|
170
176
|
});
|
|
177
|
+
if (config.strokeDash !== undefined || config.strokeDashMap !== undefined ||
|
|
178
|
+
previous.strokeDashMap !== undefined) {
|
|
179
|
+
next = next.editGraphics({ target: id, property: "strokeDash", value: resolveGuideDashes(resolved.values, config) });
|
|
180
|
+
}
|
|
171
181
|
// Structural dependency plans own grids during a scale/role handoff.
|
|
172
|
-
return explicitMode ? applyMaterializationPlan(next, planAxisTickGridRematerialization(next, channel)) : next;
|
|
182
|
+
return explicitMode ? applyMaterializationPlan(next, planAxisTickGridRematerialization(next, channel, axisId)) : next;
|
|
173
183
|
}));
|
|
174
184
|
}
|
|
175
185
|
|
|
@@ -180,10 +190,12 @@ function makeCreate(channel) {
|
|
|
180
190
|
const edit = channel === "x" ? "editXAxisTicks" : "editYAxisTicks";
|
|
181
191
|
return action({ op, description: `Create concrete ${channel}-axis ticks.` }, withGuideLayoutValidation(function (args = {}) {
|
|
182
192
|
validateOptions(args, op, true);
|
|
193
|
+
const selected = selectCartesianAxis(this, channel, args, { create: true });
|
|
194
|
+
const axisId = selected.id; args = selected.options;
|
|
183
195
|
const scale = validateUserId(args.scale ?? channel, "Scale id");
|
|
184
|
-
const existingGuide = this.semanticSpec.guides.axis?.[
|
|
196
|
+
const existingGuide = this.semanticSpec.guides.axis?.[axisId]?.scale;
|
|
185
197
|
if (existingGuide && existingGuide !== scale) throw new Error(`${op} conflicts with the existing axis scale.`);
|
|
186
|
-
const id =
|
|
198
|
+
const id = axisGraphicIds(channel, axisId)[1];
|
|
187
199
|
if (this.graphicSpec.objects[id]) throw new Error(`${op} requires missing axis ticks.`);
|
|
188
200
|
const resolvedScale = this.resolvedScales[scale];
|
|
189
201
|
const inferredValues =
|
|
@@ -194,18 +206,18 @@ function makeCreate(channel) {
|
|
|
194
206
|
: inferHistogramBoundaries(this, channel, scale);
|
|
195
207
|
const options =
|
|
196
208
|
inferredValues === undefined ? args : { ...args, values: inferredValues };
|
|
197
|
-
const config = { scale, position: defaultAxisPosition(channel), length: DEFAULTS.length, color: DEFAULTS.color, lineWidth: DEFAULTS.lineWidth, ...options, inferredValues: inferredValues !== undefined, mode: Object.hasOwn(options, "values") ? "values" : "count" };
|
|
209
|
+
const config = patchGuideDash({ scale, position: defaultAxisPosition(channel), length: DEFAULTS.length, color: DEFAULTS.color, lineWidth: DEFAULTS.lineWidth, ...options, inferredValues: inferredValues !== undefined, mode: Object.hasOwn(options, "values") ? "values" : "count" }, args);
|
|
198
210
|
if (config.mode === "values") delete config.count; else config.count ??= DEFAULTS.count;
|
|
199
|
-
validateConfig(channel, config); geometry(this, channel, config);
|
|
200
|
-
const next = this.editSemantic({ property: `guide.axis.${
|
|
211
|
+
validateConfig(channel, config); geometry(this, channel, config, axisId);
|
|
212
|
+
const next = this.editSemantic({ property: `guide.axis.${axisId}.scale`, value: scale })
|
|
201
213
|
.createGraphics({
|
|
202
214
|
id,
|
|
203
215
|
type: "line",
|
|
204
216
|
length: 0,
|
|
205
217
|
...resolvePlotGraphicPlacement(this)
|
|
206
218
|
})
|
|
207
|
-
._withGuideConfig(
|
|
208
|
-
return applyMaterializationPlan(next, planAxisTickGridRematerialization(next, channel));
|
|
219
|
+
._withGuideConfig(axisId, config)[edit](axisId === channel ? undefined : axisTargetArgs(axisId, channel));
|
|
220
|
+
return applyMaterializationPlan(next, planAxisTickGridRematerialization(next, channel, axisId));
|
|
209
221
|
}));
|
|
210
222
|
}
|
|
211
223
|
|
|
@@ -1,19 +1,24 @@
|
|
|
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 } from "../../../grammar/axisTextPlacement.js";
|
|
5
|
+
import { axisThemeTypography } from "../../theme/state.js";
|
|
6
|
+
import { canvasOverflowError } from "../../../layout/canvas.js";
|
|
1
7
|
import { isSourceOwnedText } from "../../../grammar/text.js";
|
|
2
8
|
import { withGuideLayoutValidation } from "../../../materialization/guides/layout.js";
|
|
3
9
|
import { action } from "../../../core/action.js";
|
|
4
10
|
import { validateUserId } from "../../../core/identifiers.js";
|
|
5
11
|
import {
|
|
6
12
|
validateKeys,
|
|
7
|
-
validateNonEmptyString
|
|
8
|
-
validateNonNegativeFinite
|
|
13
|
+
validateNonEmptyString
|
|
9
14
|
} from "../../../core/validation.js";
|
|
10
|
-
import {
|
|
15
|
+
import { axisScaleBindings, resolveCartesianAxisFrameBounds } from "../../../materialization/coordinateBounds.js";
|
|
11
16
|
import {
|
|
12
17
|
isTransformedScaleType,
|
|
13
18
|
mapContinuousScaleValues,
|
|
14
19
|
mapOrdinalPositionValues
|
|
15
20
|
} from "../../../grammar/scales/index.js";
|
|
16
|
-
import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
|
|
21
|
+
import { AXIS_TITLE_FONT_SIZE, DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
|
|
17
22
|
"../../../theme/defaults.js";
|
|
18
23
|
import { findDataset } from "../../../selectors/datasets.js";
|
|
19
24
|
import { formatAggregateTitle } from "../../../grammar/aggregate.js";
|
|
@@ -40,24 +45,49 @@ function titleBounds(geometry, config, text, profile) {
|
|
|
40
45
|
return resolveTextBounds({
|
|
41
46
|
...geometry,
|
|
42
47
|
text,
|
|
48
|
+
lines: titleLines(text, config),
|
|
43
49
|
fontSize: config.fontSize,
|
|
44
50
|
fontFamily: config.fontFamily,
|
|
45
51
|
fontWeight: config.fontWeight,
|
|
46
|
-
textAlign: "center",
|
|
47
|
-
textBaseline: "middle",
|
|
52
|
+
textAlign: config.align ?? "center",
|
|
53
|
+
textBaseline: config.baseline ?? "middle",
|
|
48
54
|
rotation: config.rotation
|
|
49
55
|
}, profile);
|
|
50
56
|
}
|
|
51
57
|
|
|
52
|
-
function
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
})
|
|
58
|
+
function resolveTitleAlong(scale, config) {
|
|
59
|
+
const discrete = ["ordinal", "band", "point"].includes(scale?.type);
|
|
60
|
+
let along;
|
|
61
|
+
if (config.at === "center") {
|
|
62
|
+
along = finiteMidpoint(scale.range[0], scale.range[1]);
|
|
63
|
+
} else if (config.at === "start" || config.at === "end") {
|
|
64
|
+
along = scale.range[config.at === "start" ? 0 : 1];
|
|
65
|
+
} else {
|
|
66
|
+
if (discrete) {
|
|
67
|
+
if (!scale.domain.includes(config.at)) throw new RangeError("Axis title at value must be inside the scale domain.");
|
|
68
|
+
along = mapOrdinalPositionValues([config.at], scale)[0];
|
|
69
|
+
} else {
|
|
70
|
+
const low = Math.min(...scale.domain), high = Math.max(...scale.domain);
|
|
71
|
+
if (config.at < low || config.at > high) throw new RangeError("Axis title at value must be inside the scale domain.");
|
|
72
|
+
along = mapContinuousScaleValues([config.at], scale)[0];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return along + (config.alongOffset ?? 0);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function axisLabelBounds(program, channel, axisId = channel) {
|
|
79
|
+
const id = axisGraphicIds(channel, axisId)[2], labels = program.graphicSpec.objects[id];
|
|
80
|
+
const bounds = target => resolveConcreteGraphicBounds(
|
|
81
|
+
program.graphicSpec, target, program.materializationConfigs.textMetrics
|
|
82
|
+
);
|
|
83
|
+
return labels?.items?.map(item => bounds(item.id)) ?? (labels ? [bounds(id)] : []);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function inferredTitleOffset(program, channel, config, axisId = channel) {
|
|
87
|
+
const plot = resolveCartesianAxisFrameBounds(program, channel, config.position, config.scale, axisId);
|
|
58
88
|
const canvas = findCanvasGraphic(program)?.properties;
|
|
59
|
-
const labels = program.graphicSpec.objects[
|
|
60
|
-
? resolveConcreteGraphicBounds(program.graphicSpec,
|
|
89
|
+
const labels = program.graphicSpec.objects[axisGraphicIds(channel, axisId)[2]]
|
|
90
|
+
? resolveConcreteGraphicBounds(program.graphicSpec, axisGraphicIds(channel, axisId)[2], program.materializationConfigs.textMetrics)
|
|
61
91
|
: undefined;
|
|
62
92
|
const preferred = channel === "x" ? 42 : 52;
|
|
63
93
|
if (!plot || !canvas || !labels) return preferred;
|
|
@@ -71,24 +101,43 @@ function inferredTitleOffset(program, channel, config) {
|
|
|
71
101
|
position: config.position,
|
|
72
102
|
along: 0,
|
|
73
103
|
offset: preferred
|
|
74
|
-
}), config, program.semanticSpec.guides.axis?.[
|
|
104
|
+
}), config, program.semanticSpec.guides.axis?.[axisId]?.title ?? "", program.materializationConfigs.textMetrics);
|
|
75
105
|
const needed = positive
|
|
76
106
|
? labels[far] + 4 - bounds[near]
|
|
77
107
|
: bounds[far] - labels[near] + 4;
|
|
78
108
|
const available = positive
|
|
79
109
|
? canvas[horizontal ? "height" : "width"] - bounds[far]
|
|
80
110
|
: bounds[near];
|
|
81
|
-
|
|
111
|
+
// Font ink can extend beyond the one-em layout box, especially after
|
|
112
|
+
// rotating a middle-baseline title. Keep inferred edge-clamped placement
|
|
113
|
+
// inside the Canvas; explicit offsets retain their exact geometry.
|
|
114
|
+
const inkClearance = channel === "y" &&
|
|
115
|
+
Math.abs(Math.cos(config.rotation)) < 1e-10
|
|
116
|
+
? config.fontSize / 4 : 0;
|
|
117
|
+
if (program.materializationConfigs.canvas?.plot !== undefined) {
|
|
118
|
+
return preferred + Math.max(0, needed);
|
|
119
|
+
}
|
|
120
|
+
const legacyAdjustment = Math.min(available, Math.max(0, needed));
|
|
121
|
+
if (inkClearance === 0) return preferred + legacyAdjustment;
|
|
122
|
+
const adjustment = Math.min(available - inkClearance, Math.max(0, needed));
|
|
123
|
+
const candidateBounds = titleBounds(resolveAxisTitleGeometry({
|
|
124
|
+
bounds: plot, channel, position: config.position,
|
|
125
|
+
along: resolveTitleAlong(program.resolvedScales[config.scale], config),
|
|
126
|
+
offset: preferred + adjustment
|
|
127
|
+
}), config, program.semanticSpec.guides.axis?.[axisId]?.title ?? "", program.materializationConfigs.textMetrics);
|
|
128
|
+
// Optional ink reserve must not introduce a label collision.
|
|
129
|
+
return preferred + (axisLabelBounds(program, channel, axisId).some(label => label && textBoundsIntersect(candidateBounds, label))
|
|
130
|
+
? legacyAdjustment : adjustment);
|
|
82
131
|
}
|
|
83
132
|
|
|
84
|
-
const CREATE_OPTIONS = [
|
|
133
|
+
const CREATE_OPTIONS = ["target",
|
|
85
134
|
"text", "scale", "position", "at", "offset", "rotation", "color",
|
|
86
|
-
"fontSize", "fontFamily", "fontWeight"
|
|
135
|
+
"fontSize", "fontFamily", "fontWeight", "alongOffset", "align", "baseline"
|
|
87
136
|
];
|
|
88
137
|
const EDIT_OPTIONS = CREATE_OPTIONS.filter(key => key !== "scale");
|
|
89
138
|
const DEFAULTS = {
|
|
90
139
|
color: DEFAULT_COLORS.text,
|
|
91
|
-
fontSize:
|
|
140
|
+
fontSize: AXIS_TITLE_FONT_SIZE,
|
|
92
141
|
fontFamily: DEFAULT_FONT_FAMILY,
|
|
93
142
|
fontWeight: 600
|
|
94
143
|
};
|
|
@@ -100,7 +149,7 @@ function validateText(text) {
|
|
|
100
149
|
function validateConfig(channel, config) {
|
|
101
150
|
validateAxisPosition(channel, config.position);
|
|
102
151
|
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.");
|
|
103
|
-
|
|
152
|
+
validateAxisTextPlacement(config, { title: true });
|
|
104
153
|
if (!Number.isFinite(config.rotation)) throw new TypeError("Axis title rotation must be finite radians.");
|
|
105
154
|
validateAxisTextStyle(config, "Axis title");
|
|
106
155
|
}
|
|
@@ -149,37 +198,19 @@ export function inferAxisTitleText(program, channel, scaleId) {
|
|
|
149
198
|
}
|
|
150
199
|
}
|
|
151
200
|
if (primaryTitles.size === 1) return [...primaryTitles][0];
|
|
201
|
+
if (titles.size === 0 && axisScaleBindings(program, scaleId).some(binding => binding.channel === channel)) return scaleId;
|
|
152
202
|
if (titles.size !== 1) throw new Error(`Axis title text cannot be inferred for scale "${scaleId}".`);
|
|
153
203
|
return [...titles][0];
|
|
154
204
|
}
|
|
155
205
|
|
|
156
|
-
function resolveGeometry(program, channel, config) {
|
|
206
|
+
function resolveGeometry(program, channel, config, axisId = channel) {
|
|
157
207
|
const scale = program.resolvedScales[config.scale];
|
|
158
|
-
const bounds =
|
|
159
|
-
coordinate: program.semanticSpec.guides.axis?.[channel]?.coordinate,
|
|
160
|
-
scale: config.scale,
|
|
161
|
-
channel
|
|
162
|
-
});
|
|
163
|
-
const discrete = ["ordinal", "band", "point"].includes(scale?.type);
|
|
208
|
+
const bounds = resolveCartesianAxisFrameBounds(program, channel, config.position, config.scale, axisId);
|
|
164
209
|
if ((
|
|
165
210
|
!["linear", "time", "ordinal", "band", "point"].includes(scale?.type) &&
|
|
166
211
|
!isTransformedScaleType(scale?.type)
|
|
167
212
|
) || !bounds) throw new Error("Axis title requires a supported resolved scale and Canvas bounds.");
|
|
168
|
-
|
|
169
|
-
if (config.at === "center") {
|
|
170
|
-
along = finiteMidpoint(scale.range[0], scale.range[1]);
|
|
171
|
-
} else if (config.at === "start" || config.at === "end") {
|
|
172
|
-
along = scale.range[config.at === "start" ? 0 : 1];
|
|
173
|
-
} else {
|
|
174
|
-
if (discrete) {
|
|
175
|
-
if (!scale.domain.includes(config.at)) throw new RangeError("Axis title at value must be inside the scale domain.");
|
|
176
|
-
along = mapOrdinalPositionValues([config.at], scale)[0];
|
|
177
|
-
} else {
|
|
178
|
-
const low = Math.min(...scale.domain), high = Math.max(...scale.domain);
|
|
179
|
-
if (config.at < low || config.at > high) throw new RangeError("Axis title at value must be inside the scale domain.");
|
|
180
|
-
along = mapContinuousScaleValues([config.at], scale)[0];
|
|
181
|
-
}
|
|
182
|
-
}
|
|
213
|
+
const along = resolveTitleAlong(scale, config);
|
|
183
214
|
const geometry = resolveAxisTitleGeometry({
|
|
184
215
|
bounds,
|
|
185
216
|
channel,
|
|
@@ -190,16 +221,14 @@ function resolveGeometry(program, channel, config) {
|
|
|
190
221
|
const resolvedBounds = titleBounds(
|
|
191
222
|
geometry,
|
|
192
223
|
config,
|
|
193
|
-
program.semanticSpec.guides.axis?.[
|
|
224
|
+
program.semanticSpec.guides.axis?.[axisId]?.title ?? ""
|
|
194
225
|
, program.materializationConfigs.textMetrics);
|
|
195
226
|
const canvas = findCanvasGraphic(program)?.properties;
|
|
196
227
|
if (!canvas || !textBoundsFitCanvas(resolvedBounds, canvas)) {
|
|
197
|
-
throw
|
|
228
|
+
throw canvasOverflowError(`The ${channel}-axis title does not fit the Canvas margin.`, [resolvedBounds], canvas);
|
|
198
229
|
}
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
: undefined;
|
|
202
|
-
if (labelsBounds && textBoundsIntersect(resolvedBounds, labelsBounds)) {
|
|
230
|
+
const labelBounds = axisLabelBounds(program, channel, axisId);
|
|
231
|
+
if (labelBounds.some(bounds => bounds && textBoundsIntersect(resolvedBounds, bounds))) {
|
|
203
232
|
throw new Error(`The ${channel}-axis title overlaps the axis labels.`);
|
|
204
233
|
}
|
|
205
234
|
return geometry;
|
|
@@ -207,15 +236,19 @@ function resolveGeometry(program, channel, config) {
|
|
|
207
236
|
|
|
208
237
|
function names(channel) {
|
|
209
238
|
const prefix = channel === "x" ? "X" : "Y";
|
|
210
|
-
return { create: `create${prefix}AxisTitle`, edit: `edit${prefix}AxisTitle
|
|
239
|
+
return { create: `create${prefix}AxisTitle`, edit: `edit${prefix}AxisTitle` };
|
|
211
240
|
}
|
|
212
241
|
|
|
213
242
|
function makeEdit(channel) {
|
|
214
243
|
const operation = names(channel);
|
|
215
244
|
return action({ op: operation.edit, description: `Edit the ${channel}-axis title.` }, withGuideLayoutValidation(function (args = {}) {
|
|
216
245
|
validateKeys(args, EDIT_OPTIONS, operation.edit);
|
|
217
|
-
|
|
218
|
-
const
|
|
246
|
+
const selected = selectCartesianAxis(this, channel, args, { create: false });
|
|
247
|
+
const axisId = selected.id; args = selected.options;
|
|
248
|
+
const graphic = axisGraphicIds(channel, axisId)[3];
|
|
249
|
+
validateAxisTextPlacement(args, { title: true });
|
|
250
|
+
if (this.graphicSpec.objects[graphic]?.type !== "text") throw new Error(`${operation.edit} requires an existing axis title.`);
|
|
251
|
+
const previous = this.guideConfigs.axis?.[axisId]?.title;
|
|
219
252
|
if (!previous) throw new Error(`${operation.edit} requires title configuration.`);
|
|
220
253
|
const { text, rotation: requestedRotation, ...appearance } = args;
|
|
221
254
|
const explicitText = Object.hasOwn(args, "text");
|
|
@@ -245,27 +278,27 @@ function makeEdit(channel) {
|
|
|
245
278
|
? validateText(text)
|
|
246
279
|
: config.inferredText === true
|
|
247
280
|
? inferAxisTitleText(this, channel, config.scale)
|
|
248
|
-
: this.semanticSpec.guides.axis?.[
|
|
249
|
-
if (resolvedTitle !== this.semanticSpec.guides.axis?.[
|
|
281
|
+
: this.semanticSpec.guides.axis?.[axisId]?.title;
|
|
282
|
+
if (resolvedTitle !== this.semanticSpec.guides.axis?.[axisId]?.title) {
|
|
250
283
|
next = next.editSemantic({
|
|
251
|
-
property: `guide.axis.${
|
|
284
|
+
property: `guide.axis.${axisId}.title`,
|
|
252
285
|
value: validateText(resolvedTitle)
|
|
253
286
|
});
|
|
254
287
|
}
|
|
255
|
-
const resolvedText = validateText(next.semanticSpec.guides.axis?.[
|
|
288
|
+
const resolvedText = validateText(next.semanticSpec.guides.axis?.[axisId]?.title);
|
|
256
289
|
if (config.inferredOffset) {
|
|
257
|
-
config.offset = inferredTitleOffset(next, channel, config);
|
|
290
|
+
config.offset = inferredTitleOffset(next, channel, config, axisId);
|
|
258
291
|
}
|
|
259
|
-
const geometry = resolveGeometry(next, channel, config);
|
|
260
|
-
next = next._withGuideConfig(
|
|
292
|
+
const geometry = resolveGeometry(next, channel, config, axisId);
|
|
293
|
+
next = next._withGuideConfig(axisId, "title", config);
|
|
261
294
|
const properties = {
|
|
262
295
|
x: geometry.x, y: geometry.y, text: resolvedText, fill: config.color,
|
|
263
296
|
fontSize: config.fontSize, fontFamily: config.fontFamily,
|
|
264
|
-
fontWeight: config.fontWeight, textAlign: "center", textBaseline: "middle",
|
|
297
|
+
fontWeight: config.fontWeight, textAlign: config.align ?? "center", textBaseline: config.baseline ?? "middle",
|
|
265
298
|
rotation: config.rotation
|
|
266
299
|
};
|
|
267
|
-
for (const [property, value] of Object.entries(properties)) next = next.editGraphics({ target:
|
|
268
|
-
return next;
|
|
300
|
+
for (const [property, value] of Object.entries(properties)) next = next.editGraphics({ target: graphic, property, value });
|
|
301
|
+
return materializeTextLines(next, graphic, titleLines(resolvedText, config));
|
|
269
302
|
}));
|
|
270
303
|
}
|
|
271
304
|
|
|
@@ -276,6 +309,10 @@ function makeCreate(channel) {
|
|
|
276
309
|
const operation = names(channel);
|
|
277
310
|
return action({ op: operation.create, description: `Create the ${channel}-axis title.` }, withGuideLayoutValidation(function (args = {}) {
|
|
278
311
|
validateKeys(args, CREATE_OPTIONS, operation.create);
|
|
312
|
+
const selected = selectCartesianAxis(this, channel, args, { create: true });
|
|
313
|
+
const axisId = selected.id; args = selected.options;
|
|
314
|
+
const graphic = axisGraphicIds(channel, axisId)[3];
|
|
315
|
+
validateAxisTextPlacement(args, { title: true });
|
|
279
316
|
const {
|
|
280
317
|
text: requestedText,
|
|
281
318
|
scale: requestedScale,
|
|
@@ -284,9 +321,9 @@ function makeCreate(channel) {
|
|
|
284
321
|
...appearance
|
|
285
322
|
} = args;
|
|
286
323
|
const scale = validateUserId(requestedScale ?? channel, "Scale id");
|
|
287
|
-
const guideScale = this.semanticSpec.guides.axis?.[
|
|
324
|
+
const guideScale = this.semanticSpec.guides.axis?.[axisId]?.scale;
|
|
288
325
|
if (guideScale && guideScale !== scale) throw new Error(`${operation.create} conflicts with the existing axis scale.`);
|
|
289
|
-
if (this.graphicSpec.objects[
|
|
326
|
+
if (this.graphicSpec.objects[graphic]) throw new Error(`${operation.create} requires a missing axis title.`);
|
|
290
327
|
const inferredText = !Object.hasOwn(args, "text");
|
|
291
328
|
const text = validateText(
|
|
292
329
|
requestedText ?? inferAxisTitleText(this, channel, scale)
|
|
@@ -309,21 +346,22 @@ function makeCreate(channel) {
|
|
|
309
346
|
fontSize: DEFAULTS.fontSize,
|
|
310
347
|
fontFamily: DEFAULTS.fontFamily,
|
|
311
348
|
fontWeight: DEFAULTS.fontWeight,
|
|
349
|
+
...axisThemeTypography(this.materializationConfigs.theme, "title"),
|
|
312
350
|
...appearance
|
|
313
351
|
};
|
|
314
352
|
let next = this
|
|
315
|
-
.editSemantic({ property: `guide.axis.${
|
|
316
|
-
.editSemantic({ property: `guide.axis.${
|
|
317
|
-
if (inferredOffset) config.offset = inferredTitleOffset(next, channel, config);
|
|
353
|
+
.editSemantic({ property: `guide.axis.${axisId}.scale`, value: scale })
|
|
354
|
+
.editSemantic({ property: `guide.axis.${axisId}.title`, value: text });
|
|
355
|
+
if (inferredOffset) config.offset = inferredTitleOffset(next, channel, config, axisId);
|
|
318
356
|
validateConfig(channel, config);
|
|
319
|
-
resolveGeometry(next, channel, config);
|
|
357
|
+
resolveGeometry(next, channel, config, axisId);
|
|
320
358
|
return next
|
|
321
359
|
.createGraphics({
|
|
322
|
-
id:
|
|
360
|
+
id: graphic,
|
|
323
361
|
type: "text",
|
|
324
362
|
...resolvePlotGraphicPlacement(this)
|
|
325
363
|
})
|
|
326
|
-
._withGuideConfig(
|
|
364
|
+
._withGuideConfig(axisId, "title", config)[operation.edit](axisId === channel ? undefined : axisTargetArgs(axisId, channel));
|
|
327
365
|
}));
|
|
328
366
|
}
|
|
329
367
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { registerStrokeColorLegendActions } from "./legends/continuous/stroke.js";
|
|
1
2
|
import { registerBasicCartesianAxisActions } from "./axes/index.js";
|
|
2
3
|
import { registerGuideCollectionActions } from "./guides.js";
|
|
3
4
|
import { registerBasicGridActions } from "./grids/index.js";
|
|
@@ -15,6 +16,7 @@ export function registerBasicGuideActions(ProgramClass) {
|
|
|
15
16
|
registerBasicCategoricalLegendActions(ProgramClass);
|
|
16
17
|
registerGradientLegendActions(ProgramClass);
|
|
17
18
|
registerIntervalLegendActions(ProgramClass);
|
|
19
|
+
registerStrokeColorLegendActions(ProgramClass);
|
|
18
20
|
registerSizeLegendActions(ProgramClass);
|
|
19
21
|
registerBasicGridActions(ProgramClass);
|
|
20
22
|
registerGuideCollectionActions(ProgramClass);
|