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,4 @@
|
|
|
1
|
+
import { axisTargetArgs } from "../../selectors/axes.js";
|
|
1
2
|
import { validatePolarAxisArgs } from "./polar/axes/facade.js";
|
|
2
3
|
import { prefix as polarPrefix, resolveAngle } from "./polar/axes/shared.js";
|
|
3
4
|
import { validateAxesArgs } from "./axes/axes.js";
|
|
@@ -47,24 +48,26 @@ export function scopeFacadeAxes(program, layer, args) {
|
|
|
47
48
|
}
|
|
48
49
|
const scoped = { coordinate: { id: coordinate.id, type: "cartesian" } };
|
|
49
50
|
for (const channel of ["x", "y"]) {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
51
|
+
const scope = option => {
|
|
52
|
+
if (option === false || (option === undefined && layer.encoding?.[channel] === undefined)) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
const scale = layer.encoding?.[channel]?.scale;
|
|
56
|
+
if (scale === undefined) guideConflict(`this facade has no ${channel} scale`);
|
|
57
|
+
if (option?.scale !== undefined && option.scale !== scale) {
|
|
58
|
+
guideConflict(`axes.${channel}.scale does not belong to this facade`);
|
|
59
|
+
}
|
|
60
|
+
if (option?.coordinate !== undefined && option.coordinate !== coordinate.id) {
|
|
61
|
+
guideConflict(`axes.${channel}.coordinate does not belong to this facade`);
|
|
62
|
+
}
|
|
63
|
+
const scopedOption = { ...option, scale, coordinate: coordinate.id };
|
|
64
|
+
validateAxisArgs(scopedOption, `create${channel.toUpperCase()}Axis`);
|
|
65
|
+
if (scopedOption.ticksAndLabels !== false) {
|
|
66
|
+
validateAxisTickGroupArgs(scopedOption.ticksAndLabels ?? {}, "createAxisTicksAndLabels", true);
|
|
67
|
+
}
|
|
68
|
+
return scopedOption;
|
|
69
|
+
};
|
|
70
|
+
scoped[channel] = Array.isArray(args[channel]) ? args[channel].map(scope) : scope(args[channel]);
|
|
68
71
|
}
|
|
69
72
|
return scoped;
|
|
70
73
|
}
|
|
@@ -76,21 +79,25 @@ function tickMode(config) {
|
|
|
76
79
|
|
|
77
80
|
function planAxis(program, channel, args, explicit) {
|
|
78
81
|
const prefix = channel.toUpperCase();
|
|
79
|
-
const
|
|
80
|
-
const
|
|
82
|
+
const axisId = args.id ?? channel;
|
|
83
|
+
const guide = program.semanticSpec.guides.axis?.[axisId];
|
|
84
|
+
const configs = program.guideConfigs.axis?.[axisId] ?? {};
|
|
81
85
|
if (guide === undefined && Object.keys(configs).length === 0) {
|
|
82
86
|
return [{ op: `create${prefix}Axis`, args }];
|
|
83
87
|
}
|
|
84
88
|
if (guide?.scale !== args.scale || resolveStoredGuideCoordinate(program, guide, channel) !== args.coordinate) {
|
|
85
89
|
guideConflict(`${channel} axis uses a different coordinate or scale`);
|
|
86
90
|
}
|
|
91
|
+
if (explicit.offset !== undefined && explicit.offset !== (configs.frame?.offset ?? 0)) {
|
|
92
|
+
guideConflict(`${channel} axis uses a different frame offset`);
|
|
93
|
+
}
|
|
87
94
|
const positions = [...new Set(Object.values(configs).map(config => config.position).filter(Boolean))];
|
|
88
95
|
if (positions.length > 1) guideConflict(`${channel} axis components have incompatible positions`);
|
|
89
96
|
const position = explicit.position ?? positions[0];
|
|
90
|
-
const shared = { scale: args.scale, ...(position === undefined ? {} : { position }) };
|
|
97
|
+
const shared = { ...axisTargetArgs(axisId, channel), scale: args.scale, ...(position === undefined ? {} : { position }) };
|
|
91
98
|
const explicitShared = explicit.position === undefined ? {} : { position: explicit.position };
|
|
92
99
|
const steps = guide.coordinate === undefined ? [{
|
|
93
|
-
op: "editSemantic", args: { property: `guide.axis.${
|
|
100
|
+
op: "editSemantic", args: { property: `guide.axis.${axisId}.coordinate`, value: args.coordinate }
|
|
94
101
|
}] : [];
|
|
95
102
|
const group = args.ticksAndLabels ?? {};
|
|
96
103
|
const explicitGroup = explicit.ticksAndLabels ?? {};
|
|
@@ -106,7 +113,7 @@ function planAxis(program, channel, args, explicit) {
|
|
|
106
113
|
for (const component of ["line", "ticks", "labels", "title"]) {
|
|
107
114
|
const config = configs[component];
|
|
108
115
|
if (axisComponentDisabled(args, component)) {
|
|
109
|
-
if (hasAxisComponent(program, channel, component)) guideConflict(`${channel} axis already has ${component}`);
|
|
116
|
+
if (hasAxisComponent(program, channel, component, axisId)) guideConflict(`${channel} axis already has ${component}`);
|
|
110
117
|
continue;
|
|
111
118
|
}
|
|
112
119
|
if (config === undefined) continue;
|
|
@@ -154,7 +161,9 @@ export function planFacadeAxes(program, layer, scoped, explicit = {}) {
|
|
|
154
161
|
: planPolarAxis(program, channel, scoped[channel], explicit[channel] ?? {}));
|
|
155
162
|
}
|
|
156
163
|
return ["x", "y"].flatMap(channel => scoped[channel] === false ? []
|
|
157
|
-
:
|
|
164
|
+
: Array.isArray(scoped[channel])
|
|
165
|
+
? scoped[channel].flatMap((entry, index) => planAxis(program, channel, entry, explicit[channel]?.[index] ?? {}))
|
|
166
|
+
: planAxis(program, channel, scoped[channel], explicit[channel] ?? {}));
|
|
158
167
|
}
|
|
159
168
|
|
|
160
169
|
function planPolarAxis(program, channel, args, explicit) {
|
|
@@ -4,8 +4,8 @@ import { legendResourcePolicy } from "../../materialization/guides/resources.js"
|
|
|
4
4
|
import { assertGuideOptions, guideConflict, sameGuideValue } from "./reuse.js";
|
|
5
5
|
|
|
6
6
|
function compatibleLegend(program, kind, requested, existing, explicit) {
|
|
7
|
-
const expectedScales = requested.scales ?? [requested.scale];
|
|
8
|
-
const actualScales = existing.scales ?? [existing.scale];
|
|
7
|
+
const expectedScales = kind === "colorSize" ? [requested.colorScale,requested.sizeScale] : requested.scales ?? [requested.scale];
|
|
8
|
+
const actualScales = kind === "colorSize" ? [existing.colorScale,existing.sizeScale] : existing.scales ?? [existing.scale];
|
|
9
9
|
if (!sameGuideValue(expectedScales, actualScales) ||
|
|
10
10
|
!sameGuideValue(requested.channels, existing.channels)) {
|
|
11
11
|
guideConflict(`${kind} legend uses different channels or scales`);
|
|
@@ -17,10 +17,18 @@ function compatibleLegend(program, kind, requested, existing, explicit) {
|
|
|
17
17
|
if (requested.symbol?.layers !== undefined && !sameGuideValue(
|
|
18
18
|
requested.symbol.layers.map(layer => layer.type), existing.symbol?.layers?.map(layer => layer.type)
|
|
19
19
|
)) guideConflict(`${kind} legend uses a different symbol recipe`);
|
|
20
|
+
if (explicit.representation !== undefined && (requested.representation ?? "gradient") !== (existing.representation ?? "gradient")) guideConflict(`${kind} legend uses a different color representation`);
|
|
21
|
+
if (requested.representation === "symbols" || kind === "colorSize") {
|
|
22
|
+
if (explicit.values !== undefined && !sameGuideValue(requested.sampling.values, existing.sampling?.values)) guideConflict(`${kind} legend uses different exact samples`);
|
|
23
|
+
if (explicit.count !== undefined && (existing.sampling?.mode !== "auto" || requested.sampling.count !== existing.sampling?.count)) guideConflict(`${kind} legend uses a different sample count`);
|
|
24
|
+
}
|
|
20
25
|
const appearance = {};
|
|
21
26
|
for (const [key, value] of Object.entries(explicit)) {
|
|
22
|
-
if (["target", "channels"].includes(key) || requested[key] === undefined) continue;
|
|
23
|
-
if (key === "
|
|
27
|
+
if (["target", "channels", "representation", "values"].includes(key) || requested[key] === undefined) continue;
|
|
28
|
+
if (key === "title") {
|
|
29
|
+
appearance.titleVisible = value !== false;
|
|
30
|
+
if (value !== false) appearance.title = value;
|
|
31
|
+
} else if (key === "symbol" && requested.symbol?.layers !== undefined) {
|
|
24
32
|
appearance.symbol = value === "auto" ? requested.symbol
|
|
25
33
|
: value.layers !== undefined ? value
|
|
26
34
|
: { layers: [{ type: requested.symbol.layers[0].type, ...value }] };
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
refreshGridConfig,
|
|
9
9
|
resolveGridConfig,
|
|
10
10
|
resolveGridGeometry,
|
|
11
|
+
resolveGridPlacement,
|
|
11
12
|
resolveGridResources,
|
|
12
13
|
validateGridCreateArgs,
|
|
13
14
|
validateGridEditArgs
|
|
@@ -60,7 +61,12 @@ function makeRematerialize(direction) {
|
|
|
60
61
|
|
|
61
62
|
const config = refreshGridConfig(this, storedConfig);
|
|
62
63
|
const geometry = resolveGridGeometry(this, config);
|
|
63
|
-
let next = this._withGridConfig(direction, config)
|
|
64
|
+
let next = this._withGridConfig(direction, config);
|
|
65
|
+
if (config.zIndex !== undefined) {
|
|
66
|
+
const placement = resolveGridPlacement(next, config);
|
|
67
|
+
next = next.editGraphics({ target: operation.graphic, remove: true }).createGraphics({ id: operation.graphic, type: "line", length: 0, ...placement });
|
|
68
|
+
}
|
|
69
|
+
next = next.editGraphics({
|
|
64
70
|
target: operation.graphic,
|
|
65
71
|
property: "length",
|
|
66
72
|
value: geometry.values.length
|
|
@@ -79,7 +85,7 @@ function makeRematerialize(direction) {
|
|
|
79
85
|
.editGraphics({
|
|
80
86
|
target: operation.graphic,
|
|
81
87
|
property: "strokeDash",
|
|
82
|
-
value: geometry.
|
|
88
|
+
value: geometry.strokeDash
|
|
83
89
|
});
|
|
84
90
|
}
|
|
85
91
|
);
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { cartesianAxisOwners } from "../../../selectors/axes.js";
|
|
2
|
+
import { axisGraphicIds } from "../../../materialization/guides/resources.js";
|
|
3
|
+
import { isNominalValue } from "../../../grammar/scales/fields.js";
|
|
4
|
+
import { patchGuideDash, validateGuideDash, normalizeGuideDashMap, resolveGuideDashes } from "../../../grammar/guideDash.js";
|
|
1
5
|
import { isSourceOwnedText } from "../../../grammar/text.js";
|
|
2
6
|
import { validateUserId } from "../../../core/identifiers.js";
|
|
3
7
|
import {
|
|
@@ -8,6 +12,7 @@ import {
|
|
|
8
12
|
} from "../../../core/validation.js";
|
|
9
13
|
import {
|
|
10
14
|
isTransformedScaleType,
|
|
15
|
+
mapOrdinalPositionValues,
|
|
11
16
|
mapContinuousScaleValues
|
|
12
17
|
} from "../../../grammar/scales/index.js";
|
|
13
18
|
import { resolveGuideCoordinateBounds } from "../../../materialization/coordinateBounds.js";
|
|
@@ -20,14 +25,15 @@ import {
|
|
|
20
25
|
} from "../tickValues.js";
|
|
21
26
|
import {
|
|
22
27
|
findGraphicParent,
|
|
28
|
+
graphicSiblings,
|
|
23
29
|
walkGraphicDrawOrder
|
|
24
30
|
} from "../../../grammar/schemas/graphicTree.js";
|
|
25
31
|
|
|
26
32
|
const GRID_OPTIONS = Object.freeze([
|
|
27
|
-
"scale", "coordinate", "count", "values", "color", "lineWidth", "strokeDash"
|
|
33
|
+
"scale", "coordinate", "count", "values", "color", "lineWidth", "strokeDash", "strokeDashMap", "zIndex", "bandPosition", "alongOffset"
|
|
28
34
|
]);
|
|
29
35
|
const GRID_EDIT_OPTIONS = Object.freeze([
|
|
30
|
-
"count", "values", "color", "lineWidth", "strokeDash"
|
|
36
|
+
"count", "values", "color", "lineWidth", "strokeDash", "strokeDashMap", "zIndex", "bandPosition", "alongOffset"
|
|
31
37
|
]);
|
|
32
38
|
const DEFAULT_STYLE = Object.freeze({
|
|
33
39
|
color: DEFAULT_COLORS.grid,
|
|
@@ -46,19 +52,11 @@ export function gridNames(direction) {
|
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
function validateStrokeDash(value) {
|
|
49
|
-
|
|
50
|
-
!Array.isArray(value) ||
|
|
51
|
-
value.length % 2 !== 0 ||
|
|
52
|
-
!value.every(item => Number.isFinite(item) && item >= 0)
|
|
53
|
-
) {
|
|
54
|
-
throw new TypeError(
|
|
55
|
-
"Grid strokeDash must be an even-length array of non-negative finite numbers."
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
return value;
|
|
55
|
+
return validateGuideDash(value, "Grid strokeDash");
|
|
59
56
|
}
|
|
60
57
|
|
|
61
58
|
export function validateGridCreateArgs(args, operation) {
|
|
59
|
+
if (Object.hasOwn(args, "strokeDashMap")) normalizeGuideDashMap(args.strokeDashMap);
|
|
62
60
|
validateOptionObject(args, GRID_OPTIONS, operation);
|
|
63
61
|
if (Array.isArray(args.values)) {
|
|
64
62
|
validateGeneratedItemLimit(args.values.length, "Grid value count");
|
|
@@ -75,20 +73,24 @@ export function validateGridCreateArgs(args, operation) {
|
|
|
75
73
|
if (Object.hasOwn(args, "values") && (
|
|
76
74
|
!Array.isArray(args.values) ||
|
|
77
75
|
args.values.length === 0 ||
|
|
78
|
-
!args.values.every(
|
|
76
|
+
!args.values.every(isNominalValue)
|
|
79
77
|
)) {
|
|
80
|
-
throw new TypeError("Grid values must be a non-empty finite number array.");
|
|
78
|
+
throw new TypeError("Grid values must be a non-empty finite number array or nominal category array; continuous grids require finite numbers.");
|
|
81
79
|
}
|
|
80
|
+
if (Object.hasOwn(args, "alongOffset") && !Number.isFinite(args.alongOffset)) throw new TypeError("Grid alongOffset must be a finite number.");
|
|
82
81
|
if (Object.hasOwn(args, "color")) {
|
|
83
82
|
validateNonEmptyString(args.color, "Grid color");
|
|
84
83
|
}
|
|
85
84
|
if (Object.hasOwn(args, "lineWidth")) {
|
|
86
85
|
validateNonNegativeFinite(args.lineWidth, "Grid lineWidth");
|
|
87
86
|
}
|
|
87
|
+
if (Object.hasOwn(args, "bandPosition") && !["center", "boundary"].includes(args.bandPosition)) throw new TypeError("Grid bandPosition must be center or boundary.");
|
|
88
|
+
if (Object.hasOwn(args, "zIndex") && !Number.isFinite(args.zIndex)) throw new TypeError("Grid zIndex must be a finite number.");
|
|
88
89
|
if (Object.hasOwn(args, "strokeDash")) validateStrokeDash(args.strokeDash);
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
export function validateGridEditArgs(args, operation) {
|
|
93
|
+
if (Object.hasOwn(args, "strokeDashMap")) normalizeGuideDashMap(args.strokeDashMap);
|
|
92
94
|
validateOptionObject(args, GRID_EDIT_OPTIONS, operation, {
|
|
93
95
|
allowEmpty: false
|
|
94
96
|
});
|
|
@@ -109,18 +111,21 @@ export function validateGridEditArgs(args, operation) {
|
|
|
109
111
|
if (Object.hasOwn(args, "values") && args.values !== "auto" && (
|
|
110
112
|
!Array.isArray(args.values) ||
|
|
111
113
|
args.values.length === 0 ||
|
|
112
|
-
!args.values.every(
|
|
114
|
+
!args.values.every(isNominalValue)
|
|
113
115
|
)) {
|
|
114
116
|
throw new TypeError(
|
|
115
117
|
'Grid values must be "auto" or a non-empty finite number array.'
|
|
116
118
|
);
|
|
117
119
|
}
|
|
120
|
+
if (Object.hasOwn(args, "alongOffset") && !Number.isFinite(args.alongOffset)) throw new TypeError("Grid alongOffset must be a finite number.");
|
|
118
121
|
if (Object.hasOwn(args, "color")) {
|
|
119
122
|
validateNonEmptyString(args.color, "Grid color");
|
|
120
123
|
}
|
|
121
124
|
if (Object.hasOwn(args, "lineWidth")) {
|
|
122
125
|
validateNonNegativeFinite(args.lineWidth, "Grid lineWidth");
|
|
123
126
|
}
|
|
127
|
+
if (Object.hasOwn(args, "bandPosition") && !["center", "boundary"].includes(args.bandPosition)) throw new TypeError("Grid bandPosition must be center or boundary.");
|
|
128
|
+
if (Object.hasOwn(args, "zIndex") && !Number.isFinite(args.zIndex)) throw new TypeError("Grid zIndex must be a finite number.");
|
|
124
129
|
if (Object.hasOwn(args, "strokeDash")) validateStrokeDash(args.strokeDash);
|
|
125
130
|
}
|
|
126
131
|
|
|
@@ -129,44 +134,30 @@ export function resolveGridResources(program, direction, args) {
|
|
|
129
134
|
let layers = program.semanticSpec.layers.filter(
|
|
130
135
|
layer => !isSourceOwnedText(layer) && layer.encoding?.[channel]?.scale !== undefined
|
|
131
136
|
);
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
if (layers.length === 0) throw new Error(`${direction} grid found no matching encoded layers.`);
|
|
146
|
-
const coordinateIds = [...new Set(layers.map(layer => layer.coordinate).filter(Boolean))];
|
|
147
|
-
if (layers.some(layer => layer.coordinate === undefined)) {
|
|
148
|
-
throw new Error(`${direction} grid requires a stored coordinate.`);
|
|
149
|
-
}
|
|
150
|
-
if (coordinateIds.length === 0) throw new Error(`Unknown grid coordinate "${requestedCoordinate}".`);
|
|
151
|
-
if (coordinateIds.length > 1) {
|
|
152
|
-
throw new Error(`${direction} grid found multiple coordinates; provide coordinate explicitly.`);
|
|
153
|
-
}
|
|
137
|
+
const requestedCoordinate = args.coordinate === undefined ? undefined : validateUserId(args.coordinate, "Grid coordinate id");
|
|
138
|
+
const requestedScale = args.scale === undefined ? undefined : validateUserId(args.scale, "Grid scale id");
|
|
139
|
+
const axes = cartesianAxisOwners(program).filter(owner => owner.channel === channel && owner.scale !== undefined && owner.coordinate !== undefined);
|
|
140
|
+
const bindings = [...layers.map(layer => ({ coordinate: layer.coordinate, scale: layer.encoding[channel].scale })),
|
|
141
|
+
...axes.map(owner => ({ coordinate: owner.coordinate, scale: owner.scale }))]
|
|
142
|
+
.filter(binding => (requestedCoordinate === undefined || binding.coordinate === requestedCoordinate) &&
|
|
143
|
+
(requestedScale === undefined || binding.scale === requestedScale));
|
|
144
|
+
if (bindings.length === 0) throw new Error(layers.length === 0
|
|
145
|
+
? `${direction} grid requires a ${channel} encoding or bound Cartesian axis.`
|
|
146
|
+
: `${direction} grid found no matching encoded layers.`);
|
|
147
|
+
if (bindings.some(binding => binding.coordinate === undefined)) throw new Error(`${direction} grid requires a stored coordinate.`);
|
|
148
|
+
const coordinateIds = [...new Set(bindings.map(binding => binding.coordinate))];
|
|
149
|
+
if (coordinateIds.length > 1) throw new Error(`${direction} grid found multiple coordinates; provide coordinate explicitly.`);
|
|
154
150
|
const coordinate = requestedCoordinate ?? coordinateIds[0];
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
const scaleIds = [...new Set(layers.map(layer => layer.encoding[channel].scale))];
|
|
160
|
-
if (scaleIds.length === 0) throw new Error(`Unknown grid scale "${requestedScale}".`);
|
|
161
|
-
if (scaleIds.length > 1) {
|
|
162
|
-
throw new Error(`${direction} grid found multiple ${channel} scales; provide scale explicitly.`);
|
|
163
|
-
}
|
|
151
|
+
if (findCoordinate(program, coordinate)?.type !== "cartesian") throw new Error(`${direction} grid requires a Cartesian coordinate.`);
|
|
152
|
+
const scaleIds = [...new Set(bindings.map(binding => binding.scale))];
|
|
153
|
+
if (scaleIds.length > 1) throw new Error(`${direction} grid found multiple ${channel} scales; provide scale explicitly.`);
|
|
164
154
|
const scale = requestedScale ?? scaleIds[0];
|
|
155
|
+
layers = layers.filter(layer => layer.coordinate === coordinate && layer.encoding[channel].scale === scale);
|
|
165
156
|
if (
|
|
166
|
-
!["linear", "time"].includes(program.resolvedScales[scale]?.type) &&
|
|
157
|
+
!["linear", "time", "band", "point"].includes(program.resolvedScales[scale]?.type) &&
|
|
167
158
|
!isTransformedScaleType(program.resolvedScales[scale]?.type)
|
|
168
159
|
) {
|
|
169
|
-
throw new Error(`${direction} grid requires resolved continuous scale "${scale}".`);
|
|
160
|
+
throw new Error(`${direction} grid requires resolved continuous, band or point scale "${scale}".`);
|
|
170
161
|
}
|
|
171
162
|
const sourceIds = new Set(layers.map(layer => layer.id));
|
|
172
163
|
const relatedLayers = [...layers, ...program.semanticSpec.layers.filter(layer =>
|
|
@@ -176,9 +167,15 @@ export function resolveGridResources(program, direction, args) {
|
|
|
176
167
|
const leader = program.materializationConfigs.labelLayouts?.[layer.id]?.leaderId;
|
|
177
168
|
return leader === undefined ? [layer.id] : [leader, layer.id];
|
|
178
169
|
}));
|
|
170
|
+
if (related.size === 0) {
|
|
171
|
+
for (const owner of axes.filter(owner => owner.scale === scale && owner.coordinate === coordinate)) {
|
|
172
|
+
for (const id of axisGraphicIds(channel, owner.id)) if (program.graphicSpec.objects[id] !== undefined) related.add(id);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
179
175
|
let before;
|
|
176
|
+
let after;
|
|
180
177
|
walkGraphicDrawOrder(program.graphicSpec, ({ id }) => {
|
|
181
|
-
if (
|
|
178
|
+
if (related.has(id)) { if (before === undefined) before = id; after = id; }
|
|
182
179
|
});
|
|
183
180
|
if (before === undefined) throw new Error(`${direction} grid requires related mark graphics.`);
|
|
184
181
|
const owner = findGraphicParent(program.graphicSpec, before);
|
|
@@ -187,6 +184,7 @@ export function resolveGridResources(program, direction, args) {
|
|
|
187
184
|
coordinate,
|
|
188
185
|
scale,
|
|
189
186
|
before,
|
|
187
|
+
after,
|
|
190
188
|
...(owner?.kind === "parent" ? { parent: owner.id } : {})
|
|
191
189
|
};
|
|
192
190
|
}
|
|
@@ -199,8 +197,11 @@ export function resolveGridConfig(program, direction, args, resources) {
|
|
|
199
197
|
: Object.hasOwn(args, "count")
|
|
200
198
|
? { mode: "count", count: args.count }
|
|
201
199
|
: inferGridTickConfig(program, resources.channel, resources.scale);
|
|
202
|
-
return {
|
|
200
|
+
return patchGuideDash({
|
|
203
201
|
direction,
|
|
202
|
+
...(args.bandPosition === undefined ? {} : { bandPosition: args.bandPosition }),
|
|
203
|
+
...(args.zIndex === undefined ? {} : { zIndex: args.zIndex }),
|
|
204
|
+
...(args.alongOffset === undefined ? {} : { alongOffset: args.alongOffset }),
|
|
204
205
|
scale: resources.scale,
|
|
205
206
|
coordinate: resources.coordinate,
|
|
206
207
|
...explicit,
|
|
@@ -208,7 +209,7 @@ export function resolveGridConfig(program, direction, args, resources) {
|
|
|
208
209
|
color: args.color ?? DEFAULT_STYLE.color,
|
|
209
210
|
lineWidth: args.lineWidth ?? DEFAULT_STYLE.lineWidth,
|
|
210
211
|
strokeDash: args.strokeDash ?? DEFAULT_STYLE.strokeDash
|
|
211
|
-
};
|
|
212
|
+
}, args);
|
|
212
213
|
}
|
|
213
214
|
|
|
214
215
|
export function refreshGridConfig(program, config) {
|
|
@@ -243,7 +244,7 @@ export function editGridConfig(previous, args) {
|
|
|
243
244
|
} else if (args.values === "auto") {
|
|
244
245
|
config.inferredValues = true;
|
|
245
246
|
}
|
|
246
|
-
return config;
|
|
247
|
+
return patchGuideDash(config, args);
|
|
247
248
|
}
|
|
248
249
|
|
|
249
250
|
export function resolveGridGeometry(program, config) {
|
|
@@ -255,23 +256,66 @@ export function resolveGridGeometry(program, config) {
|
|
|
255
256
|
channel
|
|
256
257
|
});
|
|
257
258
|
if ((
|
|
258
|
-
!["linear", "time"].includes(scale?.type) &&
|
|
259
|
+
!["linear", "time", "band", "point"].includes(scale?.type) &&
|
|
259
260
|
!isTransformedScaleType(scale?.type)
|
|
260
261
|
) || bounds === undefined) {
|
|
261
|
-
throw new Error("Grid materialization requires a continuous scale and Canvas bounds.");
|
|
262
|
+
throw new Error("Grid materialization requires a continuous, band or point scale and Canvas bounds.");
|
|
263
|
+
}
|
|
264
|
+
if (config.bandPosition !== undefined && scale.type !== "band") throw new Error("Grid bandPosition requires a band scale.");
|
|
265
|
+
if (["band", "point"].includes(scale.type)) {
|
|
266
|
+
if (config.mode === "count") throw new Error("Categorical grid values cannot use count.");
|
|
267
|
+
const values = valuesFromTickConfig(program, config);
|
|
268
|
+
if (!values.every(value => scale.domain.includes(value))) throw new RangeError("Grid values must be inside the scale domain.");
|
|
269
|
+
const centers = mapOrdinalPositionValues(values, scale);
|
|
270
|
+
const direction = scale.range[1] >= scale.range[0] ? 1 : -1;
|
|
271
|
+
const owners = new Map();
|
|
272
|
+
if (config.bandPosition === "boundary") {
|
|
273
|
+
centers.forEach((center, index) => {
|
|
274
|
+
for (const edge of [center - direction * scale.bandwidth / 2, center + direction * scale.bandwidth / 2]) {
|
|
275
|
+
if (!owners.has(edge)) owners.set(edge, values[index]);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
const positions = config.bandPosition === "boundary"
|
|
280
|
+
? [...owners.keys()].sort((a, b) => direction * (a - b)) : centers;
|
|
281
|
+
validateGeneratedItemLimit(positions.length, "Grid value count");
|
|
282
|
+
return {
|
|
283
|
+
...resolveGridLineGeometry({ direction: config.direction, values: positions, positions: positions.map(value => value + (config.alongOffset ?? 0)), bounds }),
|
|
284
|
+
strokeDash: resolveGuideDashes(config.bandPosition === "boundary" ? positions.map(value => owners.get(value)) : values, config)
|
|
285
|
+
};
|
|
262
286
|
}
|
|
263
287
|
const values = valuesFromTickConfig(program, config);
|
|
264
288
|
validateGeneratedItemLimit(values.length, "Grid value count");
|
|
265
289
|
const low = Math.min(...scale.domain);
|
|
266
290
|
const high = Math.max(...scale.domain);
|
|
267
|
-
if (!values.every(value => value >= low && value <= high)) {
|
|
291
|
+
if (!values.every(value => Number.isFinite(value) && value >= low && value <= high)) {
|
|
268
292
|
throw new RangeError("Grid values must be inside the scale domain.");
|
|
269
293
|
}
|
|
270
294
|
const positions = mapContinuousScaleValues(values, scale);
|
|
271
|
-
return resolveGridLineGeometry({
|
|
295
|
+
return { ...resolveGridLineGeometry({
|
|
272
296
|
direction: config.direction,
|
|
273
297
|
values,
|
|
274
|
-
positions,
|
|
298
|
+
positions: positions.map(value => value + (config.alongOffset ?? 0)),
|
|
275
299
|
bounds
|
|
276
|
-
});
|
|
300
|
+
}), strokeDash: resolveGuideDashes(values, config) };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
export function resolveGridPlacement(program, config) {
|
|
305
|
+
const resources = resolveGridResources(program, config.direction, { scale: config.scale, coordinate: config.coordinate });
|
|
306
|
+
const id = gridNames(config.direction).graphic;
|
|
307
|
+
const siblings = graphicSiblings(program.graphicSpec, resources.before);
|
|
308
|
+
const rank = direction => [program.guideConfigs.grid?.[direction]?.zIndex ?? 0, direction === "horizontal" ? 0 : 1];
|
|
309
|
+
const ownRank = [config.zIndex ?? 0, config.direction === "horizontal" ? 0 : 1];
|
|
310
|
+
const compare = (a, b) => a[0] - b[0] || a[1] - b[1];
|
|
311
|
+
const neighbors = ["horizontal", "vertical"].filter(direction => {
|
|
312
|
+
const other = gridNames(direction).graphic;
|
|
313
|
+
return other !== id && siblings.includes(other) && ((rank(direction)[0] > 0) === (ownRank[0] > 0));
|
|
314
|
+
}).sort((a, b) => compare(rank(a), rank(b)));
|
|
315
|
+
const greater = neighbors.find(direction => compare(rank(direction), ownRank) > 0);
|
|
316
|
+
const lower = neighbors.filter(direction => compare(rank(direction), ownRank) < 0).at(-1);
|
|
317
|
+
return {
|
|
318
|
+
...(resources.parent === undefined ? {} : { parent: resources.parent }),
|
|
319
|
+
...(greater ? { before: gridNames(greater).graphic } : lower ? { after: gridNames(lower).graphic } : ownRank[0] > 0 ? { after: resources.after } : { before: resources.before })
|
|
320
|
+
};
|
|
277
321
|
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { editFacetParentLegend } from "../../../materialization/facetGuides/edit.js";
|
|
2
|
+
|
|
3
|
+
import {patchLegendClipHeight, validateLegendClip} from "../../../grammar/legendClip.js";
|
|
4
|
+
import { LEGEND_POSITIONS } from "../../../core/vocabulary.js";
|
|
1
5
|
import { action } from "../../../core/action.js";
|
|
2
6
|
import { isPlainObject } from "../../../core/immutable.js";
|
|
3
7
|
import {
|
|
@@ -23,11 +27,11 @@ import {
|
|
|
23
27
|
resolveLegendBlock
|
|
24
28
|
} from "./target.js";
|
|
25
29
|
import { validateFontWeight } from "./categorical/validation.js";
|
|
26
|
-
import { normalizeDisplayLabelMap } from "../../../grammar/displayLabels.js";
|
|
30
|
+
import { normalizeDisplayLabelMap, normalizeNumericDisplayLabelMap } from "../../../grammar/displayLabels.js";
|
|
27
31
|
|
|
28
32
|
const OPTIONS = Object.freeze([
|
|
29
33
|
"target", "channel", "title", "values", "count", "order", "gap", "text", "symbol",
|
|
30
|
-
"labelMap"
|
|
34
|
+
"labelMap", "position", "clipHeight"
|
|
31
35
|
]);
|
|
32
36
|
const TEXT_OPTIONS = Object.freeze(["fontSize", "fontFamily", "fontWeight", "color"]);
|
|
33
37
|
const SYMBOL_OPTIONS = Object.freeze(["size", "fill", "stroke", "strokeWidth", "opacity"]);
|
|
@@ -63,10 +67,12 @@ function categoricalSupports(config, property) {
|
|
|
63
67
|
|
|
64
68
|
function validateSymbolSupport(descriptor, property) {
|
|
65
69
|
const { channels, family, kind, config } = descriptor;
|
|
70
|
+
if(kind === "colorSize" && ["fill","size"].includes(property)) throw new Error("Color-size symbols cannot override mapped fill or size.");
|
|
66
71
|
if (family === "gradient") {
|
|
67
72
|
throw new Error(`editLegendBlock symbol.${property} is unsupported for gradient blocks.`);
|
|
68
73
|
}
|
|
69
74
|
if (family === "categorical") {
|
|
75
|
+
if (property === "size" && channels.includes("size")) throw new Error("Combined size-shape block cannot override mapped size.");
|
|
70
76
|
if (property === "fill" && channels.includes("color")) {
|
|
71
77
|
throw new Error("editLegendBlock symbol.fill conflicts with the color mapping.");
|
|
72
78
|
}
|
|
@@ -78,6 +84,7 @@ function validateSymbolSupport(descriptor, property) {
|
|
|
78
84
|
}
|
|
79
85
|
return;
|
|
80
86
|
}
|
|
87
|
+
if (kind === "gradient" && property === "fill") throw new Error("editLegendBlock symbol.fill conflicts with the color mapping.");
|
|
81
88
|
if (kind === "size" && property === "size") {
|
|
82
89
|
throw new Error("editLegendBlock symbol.size conflicts with the size mapping.");
|
|
83
90
|
}
|
|
@@ -124,13 +131,21 @@ function normalizeSymbolPatch(value, descriptor) {
|
|
|
124
131
|
return { ...value };
|
|
125
132
|
}
|
|
126
133
|
|
|
134
|
+
function normalizeBlockLabelMap(descriptor, map) {
|
|
135
|
+
if (["size","colorSize"].includes(descriptor.kind) && descriptor.family === "sampled") {
|
|
136
|
+
return normalizeNumericDisplayLabelMap(map, "editLegendBlock labelMap");
|
|
137
|
+
}
|
|
138
|
+
if (descriptor.family !== "categorical") {
|
|
139
|
+
throw new Error("editLegendBlock labelMap requires a categorical or continuous size block.");
|
|
140
|
+
}
|
|
141
|
+
return normalizeDisplayLabelMap(map, "editLegendBlock labelMap");
|
|
142
|
+
}
|
|
143
|
+
|
|
127
144
|
export function validateLegendBlockOverride(descriptor, override) {
|
|
145
|
+
if (override.position !== undefined && !LEGEND_POSITIONS.includes(override.position)) throw new Error("Invalid local legend position.");
|
|
128
146
|
if (override.symbol !== undefined) normalizeSymbolPatch(override.symbol, descriptor);
|
|
129
147
|
if (override.labelMap !== undefined) {
|
|
130
|
-
|
|
131
|
-
throw new Error("editLegendBlock labelMap requires a categorical block.");
|
|
132
|
-
}
|
|
133
|
-
normalizeDisplayLabelMap(override.labelMap, "editLegendBlock labelMap");
|
|
148
|
+
normalizeBlockLabelMap(descriptor, override.labelMap);
|
|
134
149
|
}
|
|
135
150
|
return override;
|
|
136
151
|
}
|
|
@@ -175,6 +190,8 @@ export function patchLegendBlockOverride(config, key, patch) {
|
|
|
175
190
|
entry = withoutEmptyNested(entry, "symbol", patch.symbol);
|
|
176
191
|
}
|
|
177
192
|
if (Object.hasOwn(patch, "labelMap")) entry.labelMap = patch.labelMap;
|
|
193
|
+
if (Object.hasOwn(patch, "position")) entry.position = patch.position;
|
|
194
|
+
if (patch.removePosition === true) delete entry.position;
|
|
178
195
|
if (patch.removeLabelMap === true) delete entry.labelMap;
|
|
179
196
|
const blockOverrides = { ...(config.blockOverrides ?? {}) };
|
|
180
197
|
if (Object.keys(entry).length === 0) delete blockOverrides[key];
|
|
@@ -207,7 +224,7 @@ function applyCategoricalSymbol(symbol, patch) {
|
|
|
207
224
|
}
|
|
208
225
|
|
|
209
226
|
export function resolveEffectiveLegendBlockConfig(program, kind, config) {
|
|
210
|
-
const channels = kind === "series" || kind === "color" || kind === "stroke"
|
|
227
|
+
const channels = kind === "colorSize" ? ["color","size"] : kind === "series" || kind === "color" || kind === "stroke"
|
|
211
228
|
? config.channels
|
|
212
229
|
: [{ size: "size", opacity: "opacity", strokeWidth: "strokeWidth",
|
|
213
230
|
gradient: "color", interval: "color", strokeGradient: "stroke",
|
|
@@ -232,6 +249,7 @@ export function resolveEffectiveLegendBlockConfig(program, kind, config) {
|
|
|
232
249
|
: {})
|
|
233
250
|
};
|
|
234
251
|
if (override.symbol === undefined) return effective;
|
|
252
|
+
if(kind === "colorSize") return {...effective,symbol:{...config.symbol,...override.symbol}};
|
|
235
253
|
if (["series", "color", "stroke"].includes(kind)) {
|
|
236
254
|
return {
|
|
237
255
|
...effective,
|
|
@@ -239,7 +257,7 @@ export function resolveEffectiveLegendBlockConfig(program, kind, config) {
|
|
|
239
257
|
blockSymbol: override.symbol
|
|
240
258
|
};
|
|
241
259
|
}
|
|
242
|
-
if (kind === "opacity") {
|
|
260
|
+
if (kind === "opacity" || kind === "gradient" && config.representation === "symbols") {
|
|
243
261
|
return {
|
|
244
262
|
...effective,
|
|
245
263
|
symbol: {
|
|
@@ -289,6 +307,8 @@ export function reconcileLegendBlockTitleGraphic(program, kind, visible) {
|
|
|
289
307
|
}
|
|
290
308
|
|
|
291
309
|
const SPECIALIZED_SYMBOLS = Object.freeze({
|
|
310
|
+
colorSize: Object.freeze({id:"colorSizeLegendSymbols",type:"circle"}),
|
|
311
|
+
gradient: Object.freeze({ id: "colorGradientSymbols", type: "circle" }),
|
|
292
312
|
size: Object.freeze({ id: "sizeLegendSymbols", type: "circle" }),
|
|
293
313
|
opacity: Object.freeze({ id: "opacityLegendSymbols", type: "circle" }),
|
|
294
314
|
strokeWidth: Object.freeze({ id: "strokeWidthLegendSymbols", type: "line" }),
|
|
@@ -324,6 +344,13 @@ function recreateSymbolGraphics(program, descriptor) {
|
|
|
324
344
|
|
|
325
345
|
function normalizeBlockPatch(args, descriptor) {
|
|
326
346
|
const patch = {};
|
|
347
|
+
if (Object.hasOwn(args, "position")) {
|
|
348
|
+
if (args.position === "inherit") patch.removePosition = true;
|
|
349
|
+
else {
|
|
350
|
+
if (!LEGEND_POSITIONS.includes(args.position)) throw new Error("Legend block position requires an anchor or inherit.");
|
|
351
|
+
patch.position = args.position;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
327
354
|
if (Object.hasOwn(args, "title")) {
|
|
328
355
|
if (typeof args.title !== "string") {
|
|
329
356
|
throw new TypeError("editLegendBlock title must be a string.");
|
|
@@ -339,26 +366,21 @@ function normalizeBlockPatch(args, descriptor) {
|
|
|
339
366
|
patch.symbol = normalizeSymbolPatch(args.symbol, descriptor);
|
|
340
367
|
}
|
|
341
368
|
if (Object.hasOwn(args, "labelMap")) {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
369
|
+
// Validate eligibility even for a reset, without introducing an empty map owner.
|
|
370
|
+
const map = normalizeBlockLabelMap(descriptor, args.labelMap === "auto" ? [] : args.labelMap);
|
|
345
371
|
if (args.labelMap === "auto") patch.removeLabelMap = true;
|
|
346
|
-
else
|
|
347
|
-
patch.labelMap = normalizeDisplayLabelMap(
|
|
348
|
-
args.labelMap,
|
|
349
|
-
"editLegendBlock labelMap"
|
|
350
|
-
);
|
|
351
|
-
}
|
|
372
|
+
else patch.labelMap = map;
|
|
352
373
|
}
|
|
353
374
|
return patch;
|
|
354
375
|
}
|
|
355
376
|
|
|
356
377
|
export const editLegendBlock = /* @__PURE__ */ action(
|
|
357
378
|
{
|
|
358
|
-
op: "editLegendBlock",
|
|
379
|
+
op: "editLegendBlock", scope: "any",
|
|
359
380
|
description: "Edit one logical legend block selected by channel."
|
|
360
381
|
},
|
|
361
382
|
withGuideLayoutValidation(function (args = {}) {
|
|
383
|
+
if (this.compositionSpec !== undefined) return editFacetParentLegend(this, "editLegendBlock", args);
|
|
362
384
|
validateOptionObject(args, OPTIONS, "editLegendBlock");
|
|
363
385
|
if (!Object.hasOwn(args, "target") || typeof args.target !== "string") {
|
|
364
386
|
throw new TypeError("editLegendBlock target must be an explicit legend target id.");
|
|
@@ -375,7 +397,9 @@ export const editLegendBlock = /* @__PURE__ */ action(
|
|
|
375
397
|
throw new Error("editLegendBlock values and count require a sampled legend block.");
|
|
376
398
|
}
|
|
377
399
|
const patch = normalizeBlockPatch(args, descriptor);
|
|
378
|
-
let config =
|
|
400
|
+
let config = Object.hasOwn(args, "clipHeight")
|
|
401
|
+
? patchLegendClipHeight(descriptor.config, args.clipHeight) : descriptor.config;
|
|
402
|
+
validateLegendClip(descriptor.kind, config);
|
|
379
403
|
const blockOverrides = patchLegendBlockOverride(config, descriptor.key, patch);
|
|
380
404
|
if (blockOverrides === undefined) {
|
|
381
405
|
const { blockOverrides: _removed, ...rest } = config;
|
|
@@ -421,6 +445,8 @@ export const editLegendBlock = /* @__PURE__ */ action(
|
|
|
421
445
|
}
|
|
422
446
|
next = reconcileLegendBlockTitleGraphic(next, descriptor.kind,
|
|
423
447
|
effective.titleVisible !== false);
|
|
424
|
-
return
|
|
448
|
+
return Object.hasOwn(args, "position")
|
|
449
|
+
? next.editLegend({target: args.target, channel: args.channel, position: args.position})
|
|
450
|
+
: next.rematerializeLegend();
|
|
425
451
|
})
|
|
426
452
|
);
|