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,15 +1,20 @@
|
|
|
1
|
+
import { selectCartesianAxis, axisTargetArgs } from "../../../selectors/axes.js";
|
|
2
|
+
import { axisGraphicIds } from "../../../materialization/guides/resources.js";
|
|
3
|
+
import { axisThemeTypography } from "../../theme/state.js";
|
|
4
|
+
import { validateAxisTextPlacement } from "../../../grammar/axisTextPlacement.js";
|
|
5
|
+
import { canvasOverflowError } from "../../../layout/canvas.js";
|
|
1
6
|
import { withGuideLayoutValidation } from "../../../materialization/guides/layout.js";
|
|
2
7
|
import { action } from "../../../core/action.js";
|
|
3
8
|
import { validateUserId } from "../../../core/identifiers.js";
|
|
4
9
|
import {
|
|
5
10
|
sameOrderedValues,
|
|
6
11
|
validateGeneratedItemLimit,
|
|
12
|
+
validateTextStyle,
|
|
7
13
|
validateOptionObject,
|
|
8
|
-
validateNonEmptyString,
|
|
9
14
|
validateNonNegativeFinite,
|
|
10
15
|
validatePositiveFinite
|
|
11
16
|
} from "../../../core/validation.js";
|
|
12
|
-
import {
|
|
17
|
+
import { resolveCartesianAxisFrameBounds } from "../../../materialization/coordinateBounds.js";
|
|
13
18
|
import {
|
|
14
19
|
isTransformedScaleType,
|
|
15
20
|
mapContinuousScaleValues,
|
|
@@ -19,7 +24,7 @@ import {
|
|
|
19
24
|
import { formatTimeTick, formatTimeTicks } from "../../../grammar/ticks.js";
|
|
20
25
|
import { resolveRotation } from "../../../grammar/rotation.js";
|
|
21
26
|
import { valuesFromTickConfig } from "../tickValues.js";
|
|
22
|
-
import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
|
|
27
|
+
import { AXIS_LABEL_FONT_SIZES, DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
|
|
23
28
|
"../../../theme/defaults.js";
|
|
24
29
|
import {
|
|
25
30
|
defaultAxisPosition,
|
|
@@ -40,19 +45,21 @@ import { resolveConcreteGraphicBounds } from
|
|
|
40
45
|
import { wrapText } from "../../../layout/text.js";
|
|
41
46
|
import {
|
|
42
47
|
normalizeDisplayLabelMap,
|
|
48
|
+
normalizeDisplayColorMap,
|
|
49
|
+
resolveDisplayLabelColor,
|
|
43
50
|
resolveDisplayLabel
|
|
44
51
|
} from "../../../grammar/displayLabels.js";
|
|
45
52
|
|
|
46
53
|
const OPTIONS = [
|
|
47
|
-
"scale", "position", "count", "values", "offset", "format", "color",
|
|
54
|
+
"target", "scale", "position", "count", "values", "offset", "format", "color",
|
|
48
55
|
"fontSize", "fontFamily", "fontWeight", "rotation", "maxWidth", "wrap",
|
|
49
|
-
"lineHeight", "overlap", "labelMap"
|
|
56
|
+
"lineHeight", "overlap", "labelMap", "alongOffset", "flush", "colorMap", "align"
|
|
50
57
|
];
|
|
51
58
|
|
|
52
59
|
const DEFAULTS = {
|
|
53
60
|
count: 5,
|
|
54
61
|
color: DEFAULT_COLORS.text,
|
|
55
|
-
fontSize:
|
|
62
|
+
fontSize: AXIS_LABEL_FONT_SIZES.cartesian,
|
|
56
63
|
fontFamily: DEFAULT_FONT_FAMILY,
|
|
57
64
|
fontWeight: "normal",
|
|
58
65
|
rotation: 0,
|
|
@@ -78,28 +85,24 @@ function normalizeLabelLayout(config, args, operation) {
|
|
|
78
85
|
return next;
|
|
79
86
|
}
|
|
80
87
|
|
|
81
|
-
export
|
|
82
|
-
validateNonEmptyString(config.color, `${label} color`);
|
|
83
|
-
validatePositiveFinite(config.fontSize, `${label} fontSize`);
|
|
84
|
-
validateNonEmptyString(config.fontFamily, `${label} fontFamily`);
|
|
85
|
-
if (typeof config.fontWeight !== "string" &&
|
|
86
|
-
!Number.isFinite(config.fontWeight)) {
|
|
87
|
-
throw new TypeError(`${label} fontWeight must be a string or number.`);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
88
|
+
export { validateTextStyle as validateAxisTextStyle };
|
|
90
89
|
|
|
91
90
|
function validateOptions(args, operation, create) {
|
|
91
|
+
validateAxisTextPlacement(args);
|
|
92
92
|
validateOptionObject(
|
|
93
93
|
args,
|
|
94
94
|
create ? OPTIONS : OPTIONS.filter(key => key !== "scale"),
|
|
95
95
|
operation
|
|
96
96
|
);
|
|
97
|
+
if (Object.hasOwn(args, "flush") && typeof args.flush !== "boolean") throw new TypeError("Label flush must be a boolean.");
|
|
98
|
+
if (Object.hasOwn(args, "alongOffset") && !Number.isFinite(args.alongOffset)) throw new TypeError("Label alongOffset must be a finite number.");
|
|
97
99
|
if (Object.hasOwn(args, "count") && Object.hasOwn(args, "values")) {
|
|
98
100
|
throw new Error(`${operation} cannot use count and values together.`);
|
|
99
101
|
}
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
function validateConfig(channel, config) {
|
|
105
|
+
validateAxisTextPlacement(config);
|
|
103
106
|
validateAxisPosition(channel, config.position);
|
|
104
107
|
if (config.mode === "count") {
|
|
105
108
|
if (!Number.isInteger(config.count) || config.count <= 0) throw new RangeError("Label count must be a positive integer.");
|
|
@@ -116,9 +119,11 @@ function validateConfig(channel, config) {
|
|
|
116
119
|
if (config.mode === "values") {
|
|
117
120
|
validateGeneratedItemLimit(config.values.length, "Label value count");
|
|
118
121
|
}
|
|
122
|
+
if (config.alongOffset !== undefined && !Number.isFinite(config.alongOffset)) throw new TypeError("Label alongOffset must be a finite number.");
|
|
119
123
|
validateNonNegativeFinite(config.offset, "Label offset");
|
|
120
|
-
|
|
124
|
+
validateTextStyle(config, "Label");
|
|
121
125
|
validateAxisFormat(config.format);
|
|
126
|
+
if (config.flush === true && config.rotation !== 0) throw new Error("Label flush requires unrotated Cartesian labels.");
|
|
122
127
|
if (!Number.isFinite(config.rotation)) {
|
|
123
128
|
throw new TypeError("Label rotation must resolve to finite radians.");
|
|
124
129
|
}
|
|
@@ -140,24 +145,30 @@ function validateConfig(channel, config) {
|
|
|
140
145
|
}
|
|
141
146
|
}
|
|
142
147
|
|
|
143
|
-
function
|
|
144
|
-
if (!Object.hasOwn(args, "labelMap")) return config;
|
|
145
|
-
const scale = program.resolvedScales[config.scale];
|
|
146
|
-
if (!["ordinal", "band", "point"].includes(scale?.type)) {
|
|
147
|
-
throw new Error(`${operation} labelMap requires a categorical scale.`);
|
|
148
|
-
}
|
|
148
|
+
function normalizeAxisLabelMaps(program, config, args, operation) {
|
|
149
149
|
const next = { ...config };
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
);
|
|
150
|
+
for (const [property, normalize] of [
|
|
151
|
+
["labelMap", normalizeDisplayLabelMap],
|
|
152
|
+
["colorMap", normalizeDisplayColorMap]
|
|
153
|
+
]) {
|
|
154
|
+
if (!Object.hasOwn(args, property)) continue;
|
|
155
|
+
if (args[property] === "auto") delete next[property];
|
|
156
|
+
else next[property] = normalize(args[property], `${operation} ${property}`);
|
|
157
157
|
}
|
|
158
|
+
validateAxisLabelMapScale(next, program.resolvedScales[config.scale], operation);
|
|
158
159
|
return next;
|
|
159
160
|
}
|
|
160
161
|
|
|
162
|
+
function validateAxisLabelMapScale(config, scale, operation = "Axis") {
|
|
163
|
+
if (["ordinal", "band", "point"].includes(scale?.type)) return;
|
|
164
|
+
for (const property of ["labelMap", "colorMap"]) {
|
|
165
|
+
if (config[property] !== undefined &&
|
|
166
|
+
!config[property].every(entry => Number.isFinite(entry.value))) {
|
|
167
|
+
throw new TypeError(`${operation} continuous ${property} values must be finite numbers or timestamps.`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
161
172
|
function assertTickCompatibility(ticks, config, operation) {
|
|
162
173
|
if (!ticks) return;
|
|
163
174
|
if (ticks.scale !== config.scale || ticks.mode !== config.mode) throw new Error(`${operation} conflicts with axis ticks.`);
|
|
@@ -218,23 +229,17 @@ function unionLabelBounds(records) {
|
|
|
218
229
|
return [...byGroup.values()];
|
|
219
230
|
}
|
|
220
231
|
|
|
221
|
-
function resolve(program, channel, config) {
|
|
232
|
+
function resolve(program, channel, config, axisId = channel) {
|
|
222
233
|
const scale = program.resolvedScales[config.scale];
|
|
223
|
-
const bounds =
|
|
224
|
-
coordinate: program.semanticSpec.guides.axis?.[channel]?.coordinate,
|
|
225
|
-
scale: config.scale,
|
|
226
|
-
channel
|
|
227
|
-
});
|
|
234
|
+
const bounds = resolveCartesianAxisFrameBounds(program, channel, config.position, config.scale, axisId);
|
|
228
235
|
const discrete = ["ordinal", "band", "point"].includes(scale?.type);
|
|
229
|
-
|
|
230
|
-
throw new Error("Axis labelMap requires a categorical scale.");
|
|
231
|
-
}
|
|
236
|
+
validateAxisLabelMapScale(config, scale);
|
|
232
237
|
if ((
|
|
233
238
|
!["linear", "time", "ordinal", "band", "point"].includes(scale?.type) &&
|
|
234
239
|
!isTransformedScaleType(scale?.type)
|
|
235
240
|
) || !bounds) throw new Error("Axis labels require a supported resolved scale and Canvas bounds.");
|
|
236
241
|
if (discrete && config.mode !== "values") throw new Error("Discrete axis labels require explicit or inferred values, not count.");
|
|
237
|
-
|
|
242
|
+
let values = valuesFromTickConfig(program, config);
|
|
238
243
|
validateGeneratedItemLimit(values.length, "Label value count");
|
|
239
244
|
if (discrete) {
|
|
240
245
|
const domainValues = new Set(scale.domain);
|
|
@@ -243,7 +248,7 @@ function resolve(program, channel, config) {
|
|
|
243
248
|
const low = Math.min(...scale.domain), high = Math.max(...scale.domain);
|
|
244
249
|
if (!values.every(value => value >= low && value <= high)) throw new RangeError("Label values must be inside the scale domain.");
|
|
245
250
|
}
|
|
246
|
-
|
|
251
|
+
let positions = discrete
|
|
247
252
|
? mapOrdinalPositionValues(values, scale)
|
|
248
253
|
: mapContinuousScaleValues(values, scale);
|
|
249
254
|
const fallback = value => formatAxisValue(
|
|
@@ -256,24 +261,39 @@ function resolve(program, channel, config) {
|
|
|
256
261
|
? formatTransformedTick(scale.type, item)
|
|
257
262
|
: String(item)
|
|
258
263
|
);
|
|
259
|
-
const
|
|
260
|
-
? values
|
|
261
|
-
:
|
|
262
|
-
|
|
263
|
-
|
|
264
|
+
const automaticTimeLabels = scale.type === "time" && config.format === "auto"
|
|
265
|
+
? formatTimeTicks(values, scale.domain)
|
|
266
|
+
: undefined;
|
|
267
|
+
let text = values.map((value, index) =>
|
|
268
|
+
resolveDisplayLabel(value, config.labelMap, raw =>
|
|
269
|
+
automaticTimeLabels?.[index] ?? fallback(raw)));
|
|
270
|
+
// Empty mapped text suppresses the label, never its raw tick or scale value.
|
|
271
|
+
// Filter all aligned arrays before wrapping so group/color indices stay dense.
|
|
272
|
+
if (text.includes("")) {
|
|
273
|
+
const visible = text.map((value, index) => value === "" ? -1 : index)
|
|
274
|
+
.filter(index => index !== -1);
|
|
275
|
+
values = visible.map(index => values[index]);
|
|
276
|
+
positions = visible.map(index => positions[index]);
|
|
277
|
+
text = visible.map(index => text[index]);
|
|
278
|
+
}
|
|
264
279
|
const geometry = {
|
|
265
280
|
values,
|
|
266
281
|
...resolveAxisLabelGeometry({
|
|
267
282
|
bounds,
|
|
268
283
|
channel,
|
|
269
284
|
position: config.position,
|
|
270
|
-
positions,
|
|
285
|
+
positions: positions.map(value => value + (config.alongOffset ?? 0)),
|
|
271
286
|
offset: config.offset
|
|
272
287
|
})
|
|
273
288
|
};
|
|
274
289
|
const resolved = expandWrappedLabels(geometry, text, config, channel, program.materializationConfigs.textMetrics);
|
|
290
|
+
if (channel === "x" && Math.abs(Math.sin(config.rotation)) > 1e-9) {
|
|
291
|
+
const outward = config.position === "bottom" ? 1 : -1;
|
|
292
|
+
resolved.textAlign = Math.sin(config.rotation) * outward < 0 ? "right" : "left";
|
|
293
|
+
resolved.textBaseline = "middle";
|
|
294
|
+
}
|
|
275
295
|
const canvas = findCanvasGraphic(program)?.properties;
|
|
276
|
-
const
|
|
296
|
+
const measuredBounds = () => resolved.text.map((value, index) => resolveTextBounds({
|
|
277
297
|
x: Array.isArray(resolved.x) ? resolved.x[index] : resolved.x,
|
|
278
298
|
y: Array.isArray(resolved.y) ? resolved.y[index] : resolved.y,
|
|
279
299
|
text: value,
|
|
@@ -284,7 +304,63 @@ function resolve(program, channel, config) {
|
|
|
284
304
|
textBaseline: resolved.textBaseline,
|
|
285
305
|
rotation: config.rotation
|
|
286
306
|
}, program.materializationConfigs.textMetrics));
|
|
287
|
-
|
|
307
|
+
let labelBounds = measuredBounds();
|
|
308
|
+
if (program.materializationConfigs.canvas?.plot !== undefined || config.rotation !== 0) {
|
|
309
|
+
const horizontal = channel === "x";
|
|
310
|
+
const near = ["bottom", "right"].includes(config.position);
|
|
311
|
+
const edge = horizontal ? bounds.y + (near ? bounds.height : 0)
|
|
312
|
+
: bounds.x + (near ? bounds.width : 0);
|
|
313
|
+
const boundKey = horizontal ? (near ? "top" : "bottom") : (near ? "left" : "right");
|
|
314
|
+
const grouped = unionLabelBounds(labelBounds.map((bounds, index) => ({ bounds, group: resolved.groups[index] })));
|
|
315
|
+
const shifts = grouped.map(item => edge + (near ? config.offset : -config.offset) - item[boundKey]);
|
|
316
|
+
const axis = horizontal ? "y" : "x";
|
|
317
|
+
const positions = resolved[axis];
|
|
318
|
+
resolved[axis] = resolved.text.map((_, index) =>
|
|
319
|
+
(Array.isArray(positions) ? positions[index] : positions) + shifts[resolved.groups[index]]);
|
|
320
|
+
labelBounds = labelBounds.map((item, index) => {
|
|
321
|
+
const delta = shifts[resolved.groups[index]];
|
|
322
|
+
return horizontal ? { ...item, top: item.top + delta, bottom: item.bottom + delta }
|
|
323
|
+
: { ...item, left: item.left + delta, right: item.right + delta };
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
if (config.flush === true) {
|
|
327
|
+
const along = channel === "x" ? "x" : "y";
|
|
328
|
+
const lowKey = channel === "x" ? "left" : "top";
|
|
329
|
+
const highKey = channel === "x" ? "right" : "bottom";
|
|
330
|
+
const low = Math.min(...scale.range) + (config.alongOffset ?? 0);
|
|
331
|
+
const high = Math.max(...scale.range) + (config.alongOffset ?? 0);
|
|
332
|
+
const grouped = unionLabelBounds(labelBounds.map((bounds, index) => ({ bounds, group: resolved.groups[index] })));
|
|
333
|
+
const shifts = grouped.map(item => {
|
|
334
|
+
if (item[highKey] - item[lowKey] > high - low + 1e-9) {
|
|
335
|
+
throw new Error(`The ${channel}-axis label cannot flush inside the scale span.`);
|
|
336
|
+
}
|
|
337
|
+
return item[lowKey] < low ? low - item[lowKey]
|
|
338
|
+
: item[highKey] > high ? high - item[highKey] : 0;
|
|
339
|
+
});
|
|
340
|
+
const positions = resolved[along];
|
|
341
|
+
resolved[along] = resolved.text.map((_, index) =>
|
|
342
|
+
(Array.isArray(positions) ? positions[index] : positions) + shifts[resolved.groups[index]]);
|
|
343
|
+
labelBounds = labelBounds.map((item, index) => {
|
|
344
|
+
const delta = shifts[resolved.groups[index]];
|
|
345
|
+
return { ...item, [lowKey]: item[lowKey] + delta, [highKey]: item[highKey] + delta };
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
if (config.align !== undefined) {
|
|
349
|
+
resolved.textAlign = config.align;
|
|
350
|
+
labelBounds = measuredBounds();
|
|
351
|
+
}
|
|
352
|
+
const rotatedOverlap = config.rotation !== 0;
|
|
353
|
+
const collisionBounds = rotatedOverlap ? resolved.text.map((text, index) => {
|
|
354
|
+
const x = Array.isArray(resolved.x) ? resolved.x[index] : resolved.x;
|
|
355
|
+
const y = Array.isArray(resolved.y) ? resolved.y[index] : resolved.y;
|
|
356
|
+
const cosine = Math.cos(config.rotation), sine = Math.sin(config.rotation);
|
|
357
|
+
return resolveTextBounds({
|
|
358
|
+
x: x * cosine + y * sine, y: -x * sine + y * cosine, text,
|
|
359
|
+
fontSize: config.fontSize, fontFamily: config.fontFamily, fontWeight: config.fontWeight,
|
|
360
|
+
textAlign: resolved.textAlign, textBaseline: resolved.textBaseline
|
|
361
|
+
}, program.materializationConfigs.textMetrics);
|
|
362
|
+
}) : labelBounds;
|
|
363
|
+
const groupedBounds = unionLabelBounds(collisionBounds.map((bounds, index) => ({
|
|
288
364
|
bounds,
|
|
289
365
|
group: resolved.groups[index]
|
|
290
366
|
})));
|
|
@@ -292,17 +368,18 @@ function resolve(program, channel, config) {
|
|
|
292
368
|
? left.left - right.left
|
|
293
369
|
: left.top - right.top);
|
|
294
370
|
if (!canvas || !labelBounds.every(item => textBoundsFitCanvas(item, canvas))) {
|
|
295
|
-
throw
|
|
371
|
+
throw canvasOverflowError(`The ${channel}-axis labels do not fit the Canvas margin.`, labelBounds, canvas);
|
|
296
372
|
}
|
|
297
|
-
if (config.overlap === "error" && orderedBounds.some((item, index) =>
|
|
298
|
-
|
|
373
|
+
if (config.overlap === "error" && orderedBounds.some((item, index) => rotatedOverlap
|
|
374
|
+
? orderedBounds.slice(0, index).some(previous => textBoundsIntersect(previous, item))
|
|
375
|
+
: index > 0 && textBoundsIntersect(orderedBounds[index - 1], item))) {
|
|
299
376
|
throw new Error(`The ${channel}-axis labels overlap each other.`);
|
|
300
377
|
}
|
|
301
|
-
const title = program.graphicSpec.objects[
|
|
302
|
-
? resolveConcreteGraphicBounds(program.graphicSpec,
|
|
378
|
+
const title = program.graphicSpec.objects[axisGraphicIds(channel, axisId)[3]]
|
|
379
|
+
? resolveConcreteGraphicBounds(program.graphicSpec, axisGraphicIds(channel, axisId)[3], program.materializationConfigs.textMetrics)
|
|
303
380
|
: undefined;
|
|
304
381
|
if (title &&
|
|
305
|
-
program.guideConfigs.axis?.[
|
|
382
|
+
program.guideConfigs.axis?.[axisId]?.title?.inferredOffset !== true &&
|
|
306
383
|
labelBounds.some(item => textBoundsIntersect(item, title))) {
|
|
307
384
|
throw new Error(`The ${channel}-axis labels overlap the axis title.`);
|
|
308
385
|
}
|
|
@@ -313,12 +390,14 @@ function makeEdit(channel) {
|
|
|
313
390
|
const op = channel === "x" ? "editXAxisLabels" : "editYAxisLabels";
|
|
314
391
|
return action({ op, description: `Edit concrete ${channel}-axis labels.` }, withGuideLayoutValidation(function (args = {}) {
|
|
315
392
|
validateOptions(args, op, false);
|
|
316
|
-
const
|
|
393
|
+
const selected = selectCartesianAxis(this, channel, args, { create: false });
|
|
394
|
+
const axisId = selected.id; args = selected.options;
|
|
395
|
+
const id = axisGraphicIds(channel, axisId)[2];
|
|
317
396
|
if (this.graphicSpec.objects[id]?.type !== "text") throw new Error(`${op} requires existing axis labels.`);
|
|
318
|
-
const previous = this.guideConfigs.axis?.[
|
|
397
|
+
const previous = this.guideConfigs.axis?.[axisId]?.labels;
|
|
319
398
|
if (!previous) throw new Error(`${op} requires label configuration.`);
|
|
320
399
|
const explicitMode = Object.hasOwn(args, "values") || Object.hasOwn(args, "count");
|
|
321
|
-
const ticks = this.guideConfigs.axis?.[
|
|
400
|
+
const ticks = this.guideConfigs.axis?.[axisId]?.ticks;
|
|
322
401
|
const inferredValues = !explicitMode && previous.inferredValues === true &&
|
|
323
402
|
ticks?.mode === "values"
|
|
324
403
|
? ticks.values
|
|
@@ -333,18 +412,20 @@ function makeEdit(channel) {
|
|
|
333
412
|
...(explicitMode ? { inferredValues: false } : {}),
|
|
334
413
|
mode
|
|
335
414
|
}, args, op);
|
|
336
|
-
config =
|
|
415
|
+
config = normalizeAxisLabelMaps(this, config, args, op);
|
|
337
416
|
if (mode === "values") delete config.count; else delete config.values;
|
|
338
417
|
validateConfig(channel, config);
|
|
339
|
-
assertTickCompatibility(this.guideConfigs.axis?.[
|
|
340
|
-
const resolved = resolve(this, channel, config);
|
|
341
|
-
let next = this._withGuideConfig(
|
|
418
|
+
assertTickCompatibility(this.guideConfigs.axis?.[axisId]?.ticks, config, op);
|
|
419
|
+
const resolved = resolve(this, channel, config, axisId);
|
|
420
|
+
let next = this._withGuideConfig(axisId, "labels", config);
|
|
342
421
|
const properties = {
|
|
343
422
|
length: resolved.text.length,
|
|
344
423
|
x: resolved.x,
|
|
345
424
|
y: resolved.y,
|
|
346
425
|
text: resolved.text,
|
|
347
|
-
fill: config.color
|
|
426
|
+
fill: config.colorMap === undefined ? config.color
|
|
427
|
+
: resolved.text.map((_, index) => resolveDisplayLabelColor(
|
|
428
|
+
resolved.values[resolved.groups[index]], config.colorMap, config.color)),
|
|
348
429
|
fontSize: config.fontSize,
|
|
349
430
|
fontFamily: config.fontFamily,
|
|
350
431
|
fontWeight: config.fontWeight,
|
|
@@ -357,10 +438,10 @@ function makeEdit(channel) {
|
|
|
357
438
|
for (const [property, value] of Object.entries(properties)) {
|
|
358
439
|
next = next.editGraphics({ target: id, property, value });
|
|
359
440
|
}
|
|
360
|
-
const titleConfig = next.guideConfigs.axis?.[
|
|
441
|
+
const titleConfig = next.guideConfigs.axis?.[axisId]?.title;
|
|
361
442
|
if (titleConfig?.inferredOffset === true) {
|
|
362
443
|
const editTitle = channel === "x" ? "editXAxisTitle" : "editYAxisTitle";
|
|
363
|
-
next = next[editTitle]();
|
|
444
|
+
next = next[editTitle](axisId === channel ? undefined : axisTargetArgs(axisId, channel));
|
|
364
445
|
}
|
|
365
446
|
return next;
|
|
366
447
|
}));
|
|
@@ -374,12 +455,14 @@ function makeCreate(channel) {
|
|
|
374
455
|
const edit = channel === "x" ? "editXAxisLabels" : "editYAxisLabels";
|
|
375
456
|
return action({ op, description: `Create concrete ${channel}-axis labels.` }, withGuideLayoutValidation(function (args = {}) {
|
|
376
457
|
validateOptions(args, op, true);
|
|
458
|
+
const selected = selectCartesianAxis(this, channel, args, { create: true });
|
|
459
|
+
const axisId = selected.id; args = selected.options;
|
|
377
460
|
const scale = validateUserId(args.scale ?? channel, "Scale id");
|
|
378
|
-
const guideScale = this.semanticSpec.guides.axis?.[
|
|
461
|
+
const guideScale = this.semanticSpec.guides.axis?.[axisId]?.scale;
|
|
379
462
|
if (guideScale && guideScale !== scale) throw new Error(`${op} conflicts with the existing axis scale.`);
|
|
380
|
-
const id =
|
|
463
|
+
const id = axisGraphicIds(channel, axisId)[2];
|
|
381
464
|
if (this.graphicSpec.objects[id]) throw new Error(`${op} requires missing axis labels.`);
|
|
382
|
-
const ticks = this.guideConfigs.axis?.[
|
|
465
|
+
const ticks = this.guideConfigs.axis?.[axisId]?.ticks;
|
|
383
466
|
const hasValues = Object.hasOwn(args, "values");
|
|
384
467
|
const hasCount = Object.hasOwn(args, "count");
|
|
385
468
|
const resolvedScale = this.resolvedScales[scale];
|
|
@@ -399,6 +482,7 @@ function makeCreate(channel) {
|
|
|
399
482
|
fontSize: DEFAULTS.fontSize,
|
|
400
483
|
fontFamily: DEFAULTS.fontFamily,
|
|
401
484
|
fontWeight: DEFAULTS.fontWeight,
|
|
485
|
+
...axisThemeTypography(this.materializationConfigs.theme, "labels"),
|
|
402
486
|
rotation: DEFAULTS.rotation,
|
|
403
487
|
overlap: DEFAULTS.overlap,
|
|
404
488
|
...args,
|
|
@@ -406,7 +490,7 @@ function makeCreate(channel) {
|
|
|
406
490
|
(ticks?.inferredValues === true || inferredDiscreteValues !== undefined),
|
|
407
491
|
mode
|
|
408
492
|
}, args, op);
|
|
409
|
-
config =
|
|
493
|
+
config = normalizeAxisLabelMaps(this, config, args, op);
|
|
410
494
|
if (mode === "values") {
|
|
411
495
|
config.values ??= ticks?.values ?? inferredDiscreteValues;
|
|
412
496
|
} else {
|
|
@@ -414,15 +498,15 @@ function makeCreate(channel) {
|
|
|
414
498
|
}
|
|
415
499
|
validateConfig(channel, config);
|
|
416
500
|
assertTickCompatibility(ticks, config, op);
|
|
417
|
-
resolve(this, channel, config);
|
|
418
|
-
return this.editSemantic({ property: `guide.axis.${
|
|
501
|
+
resolve(this, channel, config, axisId);
|
|
502
|
+
return this.editSemantic({ property: `guide.axis.${axisId}.scale`, value: scale })
|
|
419
503
|
.createGraphics({
|
|
420
504
|
id,
|
|
421
505
|
type: "text",
|
|
422
506
|
length: 0,
|
|
423
507
|
...resolvePlotGraphicPlacement(this)
|
|
424
508
|
})
|
|
425
|
-
._withGuideConfig(
|
|
509
|
+
._withGuideConfig(axisId, "labels", config)[edit](axisId === channel ? undefined : axisTargetArgs(axisId, channel));
|
|
426
510
|
}));
|
|
427
511
|
}
|
|
428
512
|
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { selectCartesianAxis, axisTargetArgs } from "../../../selectors/axes.js";
|
|
2
|
+
import { axisGraphicIds } from "../../../materialization/guides/resources.js";
|
|
3
|
+
import { requireSemanticScale } from "../../../selectors/scales.js";
|
|
1
4
|
import { editGraphicProperties } from "../../primitives/graphicProperties.js";
|
|
2
5
|
import { isSourceOwnedText } from "../../../grammar/text.js";
|
|
3
6
|
import { withGuideLayoutValidation } from "../../../materialization/guides/layout.js";
|
|
@@ -8,7 +11,7 @@ import {
|
|
|
8
11
|
validateNonEmptyString,
|
|
9
12
|
validateNonNegativeFinite
|
|
10
13
|
} from "../../../core/validation.js";
|
|
11
|
-
import {
|
|
14
|
+
import { axisScaleBindings, resolveCartesianAxisFrameBounds } from "../../../materialization/coordinateBounds.js";
|
|
12
15
|
import { DEFAULT_COLORS } from "../../../theme/defaults.js";
|
|
13
16
|
import {
|
|
14
17
|
defaultAxisPosition,
|
|
@@ -19,18 +22,18 @@ import { resolvePlotGraphicPlacement } from
|
|
|
19
22
|
"../../../materialization/graphicHierarchy.js";
|
|
20
23
|
|
|
21
24
|
const DEFAULT_STYLE = Object.freeze({ color: DEFAULT_COLORS.text, lineWidth: 1 });
|
|
22
|
-
const CREATE_OPTIONS = Object.freeze(["scale", "position", "color", "lineWidth"]);
|
|
23
|
-
const EDIT_OPTIONS = Object.freeze(["position", "color", "lineWidth"]);
|
|
25
|
+
const CREATE_OPTIONS = Object.freeze(["target", "scale", "position", "color", "lineWidth"]);
|
|
26
|
+
const EDIT_OPTIONS = Object.freeze(["target", "position", "color", "lineWidth"]);
|
|
24
27
|
|
|
25
28
|
function validateStyle({ color, lineWidth }) {
|
|
26
29
|
validateNonEmptyString(color, "Axis line color");
|
|
27
30
|
validateNonNegativeFinite(lineWidth, "Axis lineWidth");
|
|
28
31
|
}
|
|
29
32
|
|
|
30
|
-
function resolveLineRange(resolvedScale, bounds, channel) {
|
|
33
|
+
function resolveLineRange(resolvedScale, bounds, channel, explicitRange) {
|
|
31
34
|
const range = resolvedScale?.range;
|
|
32
35
|
if (
|
|
33
|
-
resolvedScale?.type !== "time" ||
|
|
36
|
+
explicitRange || resolvedScale?.type !== "time" ||
|
|
34
37
|
!Number.isFinite(resolvedScale.bandwidth)
|
|
35
38
|
) return range;
|
|
36
39
|
const plotRange = channel === "x"
|
|
@@ -41,12 +44,12 @@ function resolveLineRange(resolvedScale, bounds, channel) {
|
|
|
41
44
|
: plotRange.reverse();
|
|
42
45
|
}
|
|
43
46
|
|
|
44
|
-
function resolveGeometry(program, channel, scaleId, position) {
|
|
47
|
+
function resolveGeometry(program, channel, scaleId, position, axisId = channel) {
|
|
45
48
|
const hasConsumer = program.semanticSpec.layers.some(
|
|
46
49
|
layer => !isSourceOwnedText(layer) && layer.encoding?.[channel]?.scale === scaleId
|
|
47
50
|
);
|
|
48
51
|
|
|
49
|
-
if (!hasConsumer) {
|
|
52
|
+
if (!hasConsumer && !axisScaleBindings(program, scaleId).some(binding => binding.channel === channel)) {
|
|
50
53
|
throw new Error(
|
|
51
54
|
`Axis line requires scale "${scaleId}" on the ${channel} channel.`
|
|
52
55
|
);
|
|
@@ -54,11 +57,7 @@ function resolveGeometry(program, channel, scaleId, position) {
|
|
|
54
57
|
|
|
55
58
|
const resolvedScale = program.resolvedScales[scaleId];
|
|
56
59
|
const range = resolvedScale?.range;
|
|
57
|
-
const bounds =
|
|
58
|
-
coordinate: program.semanticSpec.guides.axis?.[channel]?.coordinate,
|
|
59
|
-
scale: scaleId,
|
|
60
|
-
channel
|
|
61
|
-
});
|
|
60
|
+
const bounds = resolveCartesianAxisFrameBounds(program, channel, position, scaleId, axisId);
|
|
62
61
|
|
|
63
62
|
if (!Array.isArray(range) || range.length !== 2 || !range.every(Number.isFinite)) {
|
|
64
63
|
throw new Error(`Axis line requires a resolved numeric scale "${scaleId}".`);
|
|
@@ -75,14 +74,14 @@ function resolveGeometry(program, channel, scaleId, position) {
|
|
|
75
74
|
bounds,
|
|
76
75
|
channel,
|
|
77
76
|
position,
|
|
78
|
-
resolveLineRange(resolvedScale, bounds, channel)
|
|
77
|
+
resolveLineRange(resolvedScale, bounds, channel, Array.isArray(requireSemanticScale(program, scaleId).range))
|
|
79
78
|
);
|
|
80
79
|
}
|
|
81
80
|
|
|
82
|
-
function axisIds(channel) {
|
|
81
|
+
function axisIds(channel, axisId = channel) {
|
|
83
82
|
return {
|
|
84
|
-
graphic:
|
|
85
|
-
guidePath: `guide.axis.${
|
|
83
|
+
graphic: axisGraphicIds(channel, axisId)[0],
|
|
84
|
+
guidePath: `guide.axis.${axisId}.scale`
|
|
86
85
|
};
|
|
87
86
|
}
|
|
88
87
|
|
|
@@ -93,24 +92,26 @@ function createEditAxisLine(channel) {
|
|
|
93
92
|
{ op: operation, description: `Edit the concrete ${channel}-axis line.` },
|
|
94
93
|
withGuideLayoutValidation(function (args = {}) {
|
|
95
94
|
validateKeys(args, EDIT_OPTIONS, operation);
|
|
96
|
-
const
|
|
95
|
+
const selected = selectCartesianAxis(this, channel, args, { create: false });
|
|
96
|
+
const axisId = selected.id; args = selected.options;
|
|
97
|
+
const { graphic } = axisIds(channel, axisId);
|
|
97
98
|
const line = this.graphicSpec.objects[graphic];
|
|
98
99
|
|
|
99
100
|
if (line?.type !== "line") {
|
|
100
101
|
throw new Error(`${operation} requires an existing ${channel}-axis line.`);
|
|
101
102
|
}
|
|
102
103
|
|
|
103
|
-
const previous = this.guideConfigs.axis?.[
|
|
104
|
+
const previous = this.guideConfigs.axis?.[axisId]?.line;
|
|
104
105
|
const position = validateAxisPosition(
|
|
105
106
|
channel,
|
|
106
107
|
args.position ?? previous?.position ?? defaultAxisPosition(channel)
|
|
107
108
|
);
|
|
108
|
-
const scaleId = this.semanticSpec.guides.axis?.[
|
|
109
|
-
const geometry = resolveGeometry(this, channel, scaleId, position);
|
|
109
|
+
const scaleId = this.semanticSpec.guides.axis?.[axisId]?.scale;
|
|
110
|
+
const geometry = resolveGeometry(this, channel, scaleId, position, axisId);
|
|
110
111
|
const color = args.color ?? line.properties.stroke;
|
|
111
112
|
const lineWidth = args.lineWidth ?? line.properties.strokeWidth;
|
|
112
113
|
validateStyle({ color, lineWidth });
|
|
113
|
-
let next = this._withGuideConfig(
|
|
114
|
+
let next = this._withGuideConfig(axisId, "line", {
|
|
114
115
|
position,
|
|
115
116
|
color,
|
|
116
117
|
lineWidth
|
|
@@ -139,6 +140,8 @@ function createAxisLine(channel) {
|
|
|
139
140
|
{ op: operation, description: `Create the concrete ${channel}-axis line.` },
|
|
140
141
|
withGuideLayoutValidation(function (args = {}) {
|
|
141
142
|
validateKeys(args, CREATE_OPTIONS, operation);
|
|
143
|
+
const selected = selectCartesianAxis(this, channel, args, { create: true });
|
|
144
|
+
const axisId = selected.id; args = selected.options;
|
|
142
145
|
const scale = validateUserId(args.scale ?? channel, "Scale id");
|
|
143
146
|
const position = validateAxisPosition(
|
|
144
147
|
channel,
|
|
@@ -147,8 +150,8 @@ function createAxisLine(channel) {
|
|
|
147
150
|
const color = args.color ?? DEFAULT_STYLE.color;
|
|
148
151
|
const lineWidth = args.lineWidth ?? DEFAULT_STYLE.lineWidth;
|
|
149
152
|
validateStyle({ color, lineWidth });
|
|
150
|
-
resolveGeometry(this, channel, scale, position);
|
|
151
|
-
const { graphic, guidePath } = axisIds(channel);
|
|
153
|
+
resolveGeometry(this, channel, scale, position, axisId);
|
|
154
|
+
const { graphic, guidePath } = axisIds(channel, axisId);
|
|
152
155
|
|
|
153
156
|
if (this.graphicSpec.objects[graphic] !== undefined) {
|
|
154
157
|
throw new Error(`${operation} requires a missing ${channel}-axis line.`);
|
|
@@ -161,7 +164,7 @@ function createAxisLine(channel) {
|
|
|
161
164
|
type: "line",
|
|
162
165
|
...resolvePlotGraphicPlacement(this)
|
|
163
166
|
})
|
|
164
|
-
[editOperation]({ position, color, lineWidth });
|
|
167
|
+
[editOperation]({ ...axisTargetArgs(axisId, channel), position, color, lineWidth });
|
|
165
168
|
})
|
|
166
169
|
);
|
|
167
170
|
}
|
|
@@ -68,7 +68,7 @@ function makeAxisAction(create) {
|
|
|
68
68
|
? { mode: "selected", dimensions: resolved.dimensions.map(dimension => ({ field: dimension.field })) }
|
|
69
69
|
: resolveParallelAxisConfigs(this, resolved.dimensions);
|
|
70
70
|
const previous = config.dimensions.find(dimension => dimension.field === args.field);
|
|
71
|
-
const updated = patchParallelAxis(previous, args, create, operation);
|
|
71
|
+
const updated = patchParallelAxis(previous, args, create, operation, this.materializationConfigs.theme);
|
|
72
72
|
const next = withTitle(withOwner(this, resolved, {
|
|
73
73
|
...config, dimensions: config.dimensions.map(dimension => dimension.field === args.field ? updated : dimension)
|
|
74
74
|
}), args, create);
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { axisThemeTypography } from "../../../theme/state.js";
|
|
1
2
|
import {
|
|
2
3
|
validateGeneratedItemLimit,
|
|
3
4
|
validateNonEmptyString,
|
|
4
5
|
validateNonNegativeFinite,
|
|
5
6
|
validateOptionObject
|
|
6
7
|
} from "../../../../core/validation.js";
|
|
7
|
-
import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from "../../../../theme/defaults.js";
|
|
8
|
+
import { AXIS_LABEL_FONT_SIZES, AXIS_TITLE_FONT_SIZE, DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from "../../../../theme/defaults.js";
|
|
8
9
|
import { validateAxisTextStyle } from "../labels.js";
|
|
9
10
|
import { validateAxisFormat } from "../policy.js";
|
|
10
11
|
|
|
@@ -20,15 +21,15 @@ const STYLE_OPTIONS = Object.freeze({
|
|
|
20
21
|
title: ["text", "offset", "color", "fontSize", "fontFamily", "fontWeight"]
|
|
21
22
|
});
|
|
22
23
|
|
|
23
|
-
export function defaultParallelAxis(field) {
|
|
24
|
+
export function defaultParallelAxis(field, theme) {
|
|
24
25
|
return {
|
|
25
26
|
field,
|
|
26
27
|
line: { color: DEFAULT_COLORS.axis, lineWidth: 1.25 },
|
|
27
28
|
ticks: { mode: "auto", length: 8, color: DEFAULT_COLORS.mutedText, lineWidth: 1 },
|
|
28
29
|
labels: { mode: "auto", offset: 9, format: "auto", color: DEFAULT_COLORS.axis,
|
|
29
|
-
fontSize:
|
|
30
|
+
fontSize: AXIS_LABEL_FONT_SIZES.parallel, fontFamily: DEFAULT_FONT_FAMILY, fontWeight: "normal", ...axisThemeTypography(theme, "labels") },
|
|
30
31
|
title: { offset: 20, color: DEFAULT_COLORS.axisTitle,
|
|
31
|
-
fontSize:
|
|
32
|
+
fontSize: AXIS_TITLE_FONT_SIZE, fontFamily: DEFAULT_FONT_FAMILY, fontWeight: 600, ...axisThemeTypography(theme, "title") }
|
|
32
33
|
};
|
|
33
34
|
}
|
|
34
35
|
|
|
@@ -98,8 +99,8 @@ function patchComponent(previous, defaults, options, part, create, operation) {
|
|
|
98
99
|
return next;
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
export function patchParallelAxis(previous, args, create, operation) {
|
|
102
|
-
const defaults = defaultParallelAxis(args.field);
|
|
102
|
+
export function patchParallelAxis(previous, args, create, operation, theme) {
|
|
103
|
+
const defaults = defaultParallelAxis(args.field, theme);
|
|
103
104
|
const next = { ...previous, field: args.field };
|
|
104
105
|
const updates = {};
|
|
105
106
|
for (const part of ["line", "title"]) {
|
|
@@ -132,6 +133,6 @@ export function resolveParallelAxisConfigs(program, dimensions) {
|
|
|
132
133
|
const previous = program.guideConfigs.axis?.parallel?.axes;
|
|
133
134
|
const mode = previous?.mode ?? "all";
|
|
134
135
|
const configs = dimensions.map(dimension => previous?.dimensions?.find(config => config.field === dimension.field) ??
|
|
135
|
-
(mode === "all" ? defaultParallelAxis(dimension.field) : { field: dimension.field }));
|
|
136
|
+
(mode === "all" ? defaultParallelAxis(dimension.field, program.materializationConfigs.theme) : { field: dimension.field }));
|
|
136
137
|
return { mode, dimensions: configs };
|
|
137
138
|
}
|