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
|
@@ -160,7 +160,7 @@ function resolveContinuousScale({
|
|
|
160
160
|
}) {
|
|
161
161
|
return {
|
|
162
162
|
type: isSequentialColor
|
|
163
|
-
?
|
|
163
|
+
? scale.type
|
|
164
164
|
: isOrdinalAppearance
|
|
165
165
|
? validateOrdinalScaleType(scale.type)
|
|
166
166
|
: scale.type === "time"
|
|
@@ -200,6 +200,8 @@ function resolveContinuousScale({
|
|
|
200
200
|
}
|
|
201
201
|
: {}),
|
|
202
202
|
...(isSequentialColor ? { interpolate: scale.interpolate ?? "rgb" } : {}),
|
|
203
|
+
...(scale.whitePoint === undefined ? {} : {whitePoint: scale.whitePoint}),
|
|
204
|
+
...(scale.gamma === undefined ? {} : {gamma: scale.gamma}),
|
|
203
205
|
...(Object.hasOwn(scale, "unknown") ? { unknown: scale.unknown } : {})
|
|
204
206
|
};
|
|
205
207
|
}
|
|
@@ -377,7 +379,7 @@ export function resolveScaleMaterialization(options) {
|
|
|
377
379
|
validateMeasuredRadiusConsumers({ scale, domain, range, consumers, markConfigs, thetaScales });
|
|
378
380
|
if (channel === "shape" && domain.length > range.length) {
|
|
379
381
|
throw new Error(
|
|
380
|
-
`Shape scale "${id}" requires at least one
|
|
382
|
+
`Shape scale "${id}" requires at least one shape entry per domain value.`
|
|
381
383
|
);
|
|
382
384
|
}
|
|
383
385
|
|
|
@@ -440,7 +442,9 @@ export function resolveScaleMaterialization(options) {
|
|
|
440
442
|
const temporalBarBand = resolveTemporalBarBand(
|
|
441
443
|
valuesByConsumer,
|
|
442
444
|
domain,
|
|
443
|
-
range
|
|
445
|
+
range,
|
|
446
|
+
markConfigs,
|
|
447
|
+
Array.isArray(scale.range)
|
|
444
448
|
);
|
|
445
449
|
if (temporalBarBand !== undefined) {
|
|
446
450
|
resolvedScale = {
|
|
@@ -16,7 +16,7 @@ export function resolveArcItems(program, layer, dataset) {
|
|
|
16
16
|
thetaScale,
|
|
17
17
|
...(radiusScale === undefined ? {} : { radiusScale }),
|
|
18
18
|
frame,
|
|
19
|
-
|
|
19
|
+
innerRadius: program.markConfigs[layer.id]?.innerRadius ?? 0
|
|
20
20
|
});
|
|
21
21
|
const graphic = program.graphicSpec.objects[layer.id];
|
|
22
22
|
const stroke = layer.encoding?.stroke;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { hasRectRadius } from "../../../grammar/roundedRect.js";
|
|
1
2
|
import { deriveSeriesBarCells } from "../../../grammar/bars/aggregate.js";
|
|
2
3
|
import { readSeriesIdentity } from "../../../grammar/pathSeries.js";
|
|
3
4
|
import {
|
|
@@ -128,9 +129,12 @@ function histogramDefinitions(program, layer, dataset) {
|
|
|
128
129
|
}
|
|
129
130
|
|
|
130
131
|
function rangedDefinitions(layer, dataset) {
|
|
132
|
+
const measure = resolveBarGrain(layer) === BAR_GRAINS.centered ? resolveBarChannels(layer).measure : undefined;
|
|
131
133
|
return dataset.values.map(row => ({
|
|
132
134
|
fields: ownFields(row),
|
|
133
|
-
channels: channelMapFromRow(row, layer),
|
|
135
|
+
channels: { ...channelMapFromRow(row, layer),
|
|
136
|
+
...(measure !== undefined && layer.encoding[`${measure}2`] === undefined
|
|
137
|
+
? { [measure]: 0, [`${measure}2`]: row[layer.encoding[measure].field] } : {}) },
|
|
134
138
|
members: [row]
|
|
135
139
|
}));
|
|
136
140
|
}
|
|
@@ -205,16 +209,13 @@ export function resolveBarItems(program, layer, dataset, selectionGrain) {
|
|
|
205
209
|
? histogramDefinitions(program, layer, dataset)
|
|
206
210
|
: grain === BAR_GRAINS.aggregate
|
|
207
211
|
? aggregateCellDefinitions(program, layer, dataset)
|
|
208
|
-
:
|
|
212
|
+
: [BAR_GRAINS.ranged, BAR_GRAINS.centered].includes(grain)
|
|
209
213
|
? rangedDefinitions(layer, dataset)
|
|
210
214
|
: undefined;
|
|
211
215
|
if (definitions === undefined) {
|
|
212
216
|
throw new Error(`Bar mark "${layer.id}" is incomplete for selection.`);
|
|
213
217
|
}
|
|
214
|
-
const graphicTypes =
|
|
215
|
-
program.markConfigs[layer.id]?.barAppearance ?? {},
|
|
216
|
-
"cornerRadius"
|
|
217
|
-
) ? ["rect", "path"] : "rect";
|
|
218
|
+
const graphicTypes = hasRectRadius(program.markConfigs[layer.id]?.barAppearance) ? ["rect", "path"] : "rect";
|
|
218
219
|
if (selectionGrain === "stack") {
|
|
219
220
|
return finalizeItems(
|
|
220
221
|
program,
|
|
@@ -59,15 +59,16 @@ export function resolveLineItems(program, layer, dataset) {
|
|
|
59
59
|
const derived = deriveLineSeries(
|
|
60
60
|
dataset.values,
|
|
61
61
|
layer,
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
: {
|
|
62
|
+
{
|
|
63
|
+
xDomain: program.resolvedScales[x?.scale]?.domain,
|
|
64
|
+
...(x?.bin === undefined ? {} : {
|
|
65
65
|
xBinBoundaries: resolveLineBins(
|
|
66
66
|
dataset.values,
|
|
67
67
|
layer,
|
|
68
68
|
requireSemanticScale(program, x.scale)
|
|
69
69
|
).boundaries
|
|
70
|
-
}
|
|
70
|
+
})
|
|
71
|
+
}
|
|
71
72
|
);
|
|
72
73
|
return finalizeItems(
|
|
73
74
|
program,
|
|
@@ -82,8 +83,8 @@ export function resolveAreaItems(program, layer, dataset) {
|
|
|
82
83
|
const transform = findUpstreamTransform(program, dataset, "density");
|
|
83
84
|
const derived = transform === undefined
|
|
84
85
|
? layer.encoding?.y?.stack === "center"
|
|
85
|
-
? deriveCenteredAreaSeries(dataset.values, layer)
|
|
86
|
-
: deriveAreaSeries(dataset.values, layer)
|
|
86
|
+
? deriveCenteredAreaSeries(dataset.values, layer, { xDomain: program.resolvedScales[layer.encoding.x?.scale]?.domain, yDomain: program.resolvedScales[layer.encoding.y?.scale]?.domain })
|
|
87
|
+
: deriveAreaSeries(dataset.values, layer, { xDomain: program.resolvedScales[layer.encoding.x?.scale]?.domain, yDomain: program.resolvedScales[layer.encoding.y?.scale]?.domain })
|
|
87
88
|
: deriveDensityAreaSeries(dataset.values, layer, transform);
|
|
88
89
|
return finalizeItems(
|
|
89
90
|
program,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { rectUsesFields } from "../../../grammar/rects.js";
|
|
2
|
-
import { readRectItemGeometry } from "../../../grammar/roundedRect.js";
|
|
2
|
+
import { hasRectRadius, readRectItemGeometry } from "../../../grammar/roundedRect.js";
|
|
3
3
|
import { resolveRectRows } from "../../rect.js";
|
|
4
4
|
import {
|
|
5
5
|
channelMapFromRow,
|
|
@@ -40,7 +40,7 @@ function resolveGradientPlotItems(program, layer, dataset) {
|
|
|
40
40
|
|
|
41
41
|
export function resolveRectItems(program, layer, dataset) {
|
|
42
42
|
const graphic = program.graphicSpec.objects[layer.id];
|
|
43
|
-
const rounded =
|
|
43
|
+
const rounded = hasRectRadius(program.markConfigs[layer.id]);
|
|
44
44
|
const compatibleCollection = rounded && graphic?.type === "collection" &&
|
|
45
45
|
graphic.items?.every(item => ["rect", "path"].includes(item.type));
|
|
46
46
|
if (
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
resolveMarkLabelPlacement
|
|
6
6
|
} from "../layout/labels.js";
|
|
7
7
|
import { resolveMarkLabelValues } from "../grammar/markLabels.js";
|
|
8
|
-
import { formatTextValue } from "../grammar/text.js";
|
|
8
|
+
import { formatTextValue, resolveTextLines } from "../grammar/text.js";
|
|
9
9
|
import { normalizePositionDatum } from "../grammar/positionDatum.js";
|
|
10
10
|
import { mapOrdinalPositionValues } from "../grammar/scales/index.js";
|
|
11
11
|
import { findDataset } from "../selectors/datasets.js";
|
|
@@ -274,12 +274,14 @@ function concreteTextItem(config, position, text, { offsets = true } = {}) {
|
|
|
274
274
|
if (text === undefined || !Number.isFinite(x) || !Number.isFinite(y)) {
|
|
275
275
|
return undefined;
|
|
276
276
|
}
|
|
277
|
+
const lines = resolveTextLines(text, config);
|
|
277
278
|
return {
|
|
278
279
|
type: "text",
|
|
279
280
|
properties: {
|
|
280
281
|
x: x + (offsets ? config.dx : 0),
|
|
281
282
|
y: y + (offsets ? config.dy : 0),
|
|
282
283
|
text,
|
|
284
|
+
...(lines === undefined ? {} : { lines }),
|
|
283
285
|
fill: config.fill,
|
|
284
286
|
opacity: config.opacity,
|
|
285
287
|
fontSize: config.fontSize,
|
|
@@ -310,6 +312,11 @@ function relativeLuminance(color) {
|
|
|
310
312
|
}
|
|
311
313
|
|
|
312
314
|
function sourceTextConfig(config, source, item) {
|
|
315
|
+
if (config.inheritColor && config.fillExplicit !== true) {
|
|
316
|
+
const fill = item.properties[config.inheritColor];
|
|
317
|
+
if (typeof fill !== "string") throw new Error(`Source mark "${source.id}" has no resolved ${config.inheritColor} color.`);
|
|
318
|
+
return { ...config, fill };
|
|
319
|
+
}
|
|
313
320
|
if (
|
|
314
321
|
!["arc", "rect"].includes(source.mark.type) ||
|
|
315
322
|
config.fillExplicit === true
|
|
@@ -383,6 +390,7 @@ function resolveSourceTextItems(program, layer, config) {
|
|
|
383
390
|
geometry: sourcePlacementGeometry(program, source, item),
|
|
384
391
|
text: {
|
|
385
392
|
text,
|
|
393
|
+
lines: resolveTextLines(text, resolvedConfig),
|
|
386
394
|
fontSize: resolvedConfig.fontSize,
|
|
387
395
|
fontFamily: resolvedConfig.fontFamily,
|
|
388
396
|
fontWeight: resolvedConfig.fontWeight,
|
|
@@ -396,7 +404,7 @@ function resolveSourceTextItems(program, layer, config) {
|
|
|
396
404
|
if (!resolved.visible) return [];
|
|
397
405
|
if (
|
|
398
406
|
resolved.anchor.startsWith("outside") &&
|
|
399
|
-
config.fillExplicit !== true
|
|
407
|
+
config.fillExplicit !== true && !config.inheritColor
|
|
400
408
|
) {
|
|
401
409
|
resolvedConfig = config;
|
|
402
410
|
}
|
|
@@ -420,7 +428,7 @@ function resolveRowTextItems(program, layer, config) {
|
|
|
420
428
|
throw new Error(`Text mark "${layer.id}" requires an existing dataset.`);
|
|
421
429
|
}
|
|
422
430
|
const dataset = applyItemMissingPolicy(layer, sourceDataset);
|
|
423
|
-
const encodings = [layer.encoding?.x, layer.encoding?.y, layer.encoding?.text];
|
|
431
|
+
const encodings = [layer.encoding?.x, layer.encoding?.y, layer.encoding?.text, layer.encoding?.color];
|
|
424
432
|
const length = encodings.some(encoding => encoding !== undefined && Object.hasOwn(encoding, "field"))
|
|
425
433
|
? dataset.values.length
|
|
426
434
|
: 1;
|
|
@@ -454,9 +462,10 @@ function resolveRowTextItems(program, layer, config) {
|
|
|
454
462
|
const rows = length === 1 && dataset.values.length === 0
|
|
455
463
|
? [undefined]
|
|
456
464
|
: dataset.values.slice(0, length);
|
|
465
|
+
const fills = resolveRowEncodingValues(program, layer, dataset, "color");
|
|
457
466
|
return rows.flatMap((row, index) => {
|
|
458
467
|
const concrete = concreteItem(
|
|
459
|
-
config,
|
|
468
|
+
fills === undefined ? config : { ...config, fill: fills[index] },
|
|
460
469
|
{ x: x[index], y: y[index] },
|
|
461
470
|
contentValue(layer.encoding.text, row),
|
|
462
471
|
layer.encoding.text.format
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { freezeOwned } from "../core/immutable.js";
|
|
2
|
+
|
|
3
|
+
// Reconcile optional derived line geometry without recreating its text graphic.
|
|
4
|
+
// The owning wrapped domain materializer records the transition.
|
|
5
|
+
export function materializeTextLines(program, id, lines) {
|
|
6
|
+
if (lines !== undefined) return program.editGraphics({ target: id, property: "lines", value: lines });
|
|
7
|
+
const graphic = program.graphicSpec.objects[id];
|
|
8
|
+
if (!Object.hasOwn(graphic.properties, "lines")) return program;
|
|
9
|
+
const { lines: _lines, ...properties } = graphic.properties;
|
|
10
|
+
return program._clone({ graphicSpec: freezeOwned({ ...program.graphicSpec,
|
|
11
|
+
objects: freezeOwned({ ...program.graphicSpec.objects,
|
|
12
|
+
[id]: freezeOwned({ ...graphic, properties: freezeOwned(properties) })
|
|
13
|
+
})
|
|
14
|
+
}) });
|
|
15
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { cartesianAxisOwners, axisTargetArgs } from "../selectors/axes.js";
|
|
2
|
+
import { axisGraphicIds } from "./guides/resources.js";
|
|
1
3
|
import { applyMaterializationPlan, buildMaterializationPlan } from "./planner.js";
|
|
2
4
|
import { withGuideLayoutTransaction } from "./guides/layout.js";
|
|
3
5
|
|
|
@@ -13,11 +15,17 @@ export function rematerializeTypography(program) {
|
|
|
13
15
|
.map(layer => ({ op: "rematerializeTextMark", args: { id: layer.id } }));
|
|
14
16
|
const guides = [];
|
|
15
17
|
for (const [id, op] of [
|
|
16
|
-
["xAxisLabels", "editXAxisLabels"], ["yAxisLabels", "editYAxisLabels"],
|
|
17
|
-
["xAxisTitle", "editXAxisTitle"], ["yAxisTitle", "editYAxisTitle"],
|
|
18
18
|
["thetaAxisLabels", "editThetaAxisLabels"], ["radialAxisLabels", "editRadialAxisLabels"],
|
|
19
19
|
["thetaAxisTitle", "editThetaAxisTitle"], ["radialAxisTitle", "editRadialAxisTitle"]
|
|
20
20
|
]) if (exists(id, op)) guides.push({ op });
|
|
21
|
+
for (const owner of cartesianAxisOwners(program)) {
|
|
22
|
+
for (const [index, component] of [[2, "Labels"], [3, "Title"]]) {
|
|
23
|
+
const op = `edit${owner.channel.toUpperCase()}Axis${component}`;
|
|
24
|
+
if (exists(axisGraphicIds(owner.channel, owner.id)[index], op)) {
|
|
25
|
+
guides.push({ op, ...(owner.named ? { args: axisTargetArgs(owner.id, owner.channel) } : {}) });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
21
29
|
const parallel = program.guideConfigs.axis?.parallel?.axes;
|
|
22
30
|
if (parallel !== undefined && typeof program.rematerializeParallelAxes === "function") {
|
|
23
31
|
guides.push({ op: "rematerializeParallelAxes", args: { target: parallel.target } });
|
package/src/persistence/codec.js
CHANGED
|
@@ -5,7 +5,7 @@ export function invalidSnapshot(message, optionPath) {
|
|
|
5
5
|
return annotateError(new TypeError(message), { code: "unsupported-format", optionPath });
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
export function encodeValue(value, path = "payload", ancestors = new WeakSet()) {
|
|
8
|
+
export function encodeValue(value, path = "payload", ancestors = new WeakSet(), shared) {
|
|
9
9
|
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
10
10
|
if (value === undefined) return ["undefined"];
|
|
11
11
|
if (typeof value === "bigint") return ["bigint", value.toString()];
|
|
@@ -17,6 +17,7 @@ export function encodeValue(value, path = "payload", ancestors = new WeakSet())
|
|
|
17
17
|
if (ancestors.has(value)) throw invalidSnapshot(`Cannot serialize a cycle at ${path}.`, path);
|
|
18
18
|
if (!Array.isArray(value) && !isPlainObject(value)) throw invalidSnapshot(`Cannot serialize a class instance at ${path}.`, path);
|
|
19
19
|
if (Object.getOwnPropertySymbols(value).length) throw invalidSnapshot(`Cannot serialize symbol keys at ${path}.`, path);
|
|
20
|
+
if (shared?.seen.has(value)) return shared.seen.get(value);
|
|
20
21
|
const keys = Object.getOwnPropertyNames(value);
|
|
21
22
|
if (Array.isArray(value) ? keys.some(key => key !== "length" && !(/^(0|[1-9]\d*)$/.test(key) && Number(key) < value.length)) :
|
|
22
23
|
keys.some(key => !Object.getOwnPropertyDescriptor(value, key).enumerable)) {
|
|
@@ -25,18 +26,48 @@ export function encodeValue(value, path = "payload", ancestors = new WeakSet())
|
|
|
25
26
|
ancestors.add(value);
|
|
26
27
|
const result = Array.isArray(value)
|
|
27
28
|
? ["array", Array.from({ length: value.length }, (_, index) => Object.hasOwn(value, index)
|
|
28
|
-
? encodeValue(value[index], `${path}[${index}]`, ancestors) : ["hole"])]
|
|
29
|
-
: ["object", Object.entries(value).map(([key, child]) => [key, encodeValue(child, `${path}.${key}`, ancestors)])];
|
|
29
|
+
? encodeValue(value[index], `${path}[${index}]`, ancestors, shared) : ["hole"])]
|
|
30
|
+
: ["object", Object.entries(value).map(([key, child]) => [key, encodeValue(child, `${path}.${key}`, ancestors, shared)])];
|
|
30
31
|
ancestors.delete(value);
|
|
32
|
+
if (shared !== undefined && Array.isArray(value) && value.length >= 32) {
|
|
33
|
+
const key = JSON.stringify(result);
|
|
34
|
+
let index = shared.keys.get(key);
|
|
35
|
+
if (index === undefined) {
|
|
36
|
+
index = shared.arrays.length;
|
|
37
|
+
shared.keys.set(key, index);
|
|
38
|
+
shared.arrays.push(result);
|
|
39
|
+
}
|
|
40
|
+
const reference = ["reference", index];
|
|
41
|
+
shared.seen.set(value, reference);
|
|
42
|
+
return reference;
|
|
43
|
+
}
|
|
31
44
|
return result;
|
|
32
45
|
}
|
|
33
46
|
|
|
34
|
-
export function
|
|
47
|
+
export function encodeSharedValue(value) {
|
|
48
|
+
const shared = { arrays: [], keys: new Map(), seen: new WeakMap() };
|
|
49
|
+
const root = encodeValue(value, "payload", new WeakSet(), shared);
|
|
50
|
+
return ["shared", shared.arrays, root];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function decodeValue(value, path = "payload", references) {
|
|
35
54
|
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
36
55
|
if (typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0)) return value;
|
|
37
56
|
const invalid = () => { throw invalidSnapshot(`Invalid encoded value at ${path}.`, path); };
|
|
38
57
|
if (!Array.isArray(value)) return invalid();
|
|
39
58
|
const [tag, payload] = value;
|
|
59
|
+
if (tag === "shared" && references === undefined && value.length === 3 && Array.isArray(payload)) {
|
|
60
|
+
const arrays = [];
|
|
61
|
+
for (const [index, encoded] of payload.entries()) {
|
|
62
|
+
if (!Array.isArray(encoded) || encoded[0] !== "array") return invalid();
|
|
63
|
+
arrays.push(decodeValue(encoded, `${path}.arrays[${index}]`, arrays));
|
|
64
|
+
}
|
|
65
|
+
return decodeValue(value[2], path, arrays);
|
|
66
|
+
}
|
|
67
|
+
if (tag === "reference" && references !== undefined && value.length === 2 &&
|
|
68
|
+
Number.isInteger(payload) && payload >= 0 && payload < references.length) {
|
|
69
|
+
return references[payload];
|
|
70
|
+
}
|
|
40
71
|
if (tag === "undefined" && value.length === 1) return undefined;
|
|
41
72
|
if (value.length !== 2) return invalid();
|
|
42
73
|
if (tag === "bigint" && typeof payload === "string" && /^(?:0|-?[1-9]\d*)$/.test(payload)) return BigInt(payload);
|
|
@@ -47,7 +78,7 @@ export function decodeValue(value, path = "payload") {
|
|
|
47
78
|
const result = new Array(payload.length);
|
|
48
79
|
payload.forEach((child, index) => {
|
|
49
80
|
if (Array.isArray(child) && child.length === 1 && child[0] === "hole") return;
|
|
50
|
-
result[index] = decodeValue(child, `${path}[${index}]
|
|
81
|
+
result[index] = decodeValue(child, `${path}[${index}]`, references ?? []);
|
|
51
82
|
});
|
|
52
83
|
return result;
|
|
53
84
|
}
|
|
@@ -56,7 +87,7 @@ export function decodeValue(value, path = "payload") {
|
|
|
56
87
|
for (const entry of payload) {
|
|
57
88
|
if (!Array.isArray(entry) || entry.length !== 2 || typeof entry[0] !== "string" || Object.hasOwn(result, entry[0])) return invalid();
|
|
58
89
|
Object.defineProperty(result, entry[0], {
|
|
59
|
-
value: decodeValue(entry[1], `${path}.${entry[0]}
|
|
90
|
+
value: decodeValue(entry[1], `${path}.${entry[0]}`, references ?? []), enumerable: true, writable: true, configurable: true
|
|
60
91
|
});
|
|
61
92
|
}
|
|
62
93
|
return result;
|
|
@@ -1,3 +1,34 @@
|
|
|
1
|
+
import { validateAuthoredFacetLegend } from "../grammar/facets/legendIntent.js";
|
|
2
|
+
import { AXIS_RESOURCE_CHANNELS } from "../core/vocabulary.js";
|
|
3
|
+
import { cartesianAxisOwners, assertCartesianAxisSideAvailable } from "../selectors/axes.js";
|
|
4
|
+
import { axisGraphicIds } from "../selectors/axes.js";
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
import {findLayer} from '../selectors/layers.js';
|
|
9
|
+
import {findCoordinate} from '../selectors/coordinates.js';
|
|
10
|
+
import {resolveColorSizeMapping,normalizeColorSizeSymbol} from '../grammar/colorSizeLegend.js';
|
|
11
|
+
import{validateLegendClip}from"../grammar/legendClip.js";
|
|
12
|
+
import {validateLegendLayerOpacity}from"../grammar/legendOpacity.js";
|
|
13
|
+
import { isSizeShapeChannels, resolveSizeShapeMapping } from "../grammar/legendCombinations.js";
|
|
14
|
+
import { describeLegendBlock } from "../grammar/legendBlocks.js";
|
|
15
|
+
import { localLegendPosition } from "../grammar/legendGroups.js";
|
|
16
|
+
import { normalizeLegendBorderDash } from "../grammar/legendBorder.js";
|
|
17
|
+
import { normalizeNumericDisplayLabelMap } from "../grammar/displayLabels.js";
|
|
18
|
+
import { isContinuousSizeScaleType } from "../grammar/scales/size.js";
|
|
19
|
+
import { validateGraphicItemLimit } from "../core/validation.js";
|
|
20
|
+
import { LEGEND_TITLE_STYLE_KEYS, validateLegendTitleAlignment } from "../grammar/legendTitleStyle.js";
|
|
21
|
+
import { validateGuideDash, normalizeGuideDashMap } from "../grammar/guideDash.js";
|
|
22
|
+
import { validateAxisTextPlacement } from "../grammar/axisTextPlacement.js";
|
|
23
|
+
import { validateAreaPaintConfig } from "../grammar/areaPaint.js";
|
|
24
|
+
import { validateOptionObject, validateKeys, validatePositiveFinite, validateNonNegativeFinite, validateNonEmptyString, validateGeneratedItemLimit } from "../core/validation.js";
|
|
25
|
+
import { validateRegressionFitStatistics, requireRegressionStatisticsSource, regressionStatisticsRows } from "../grammar/regression/statistics.js";
|
|
26
|
+
import { resolveGraphicItemCapacity } from "../selectors/graphicCapacity.js";
|
|
27
|
+
import { isContinuousColorScaleType } from "../grammar/scales/types.js";
|
|
28
|
+
import { normalizeColorInterpolationOptions } from "../grammar/scales/color.js";
|
|
29
|
+
import { normalizeTransformParameters } from "../grammar/scales/transformed.js";
|
|
30
|
+
import { validateLegendGroupState } from "../grammar/legendGroups.js";
|
|
31
|
+
import { normalizeGradientDirection, validateLegendGradient } from "../grammar/legendGradient.js";
|
|
1
32
|
import { normalizeTextMetricProfile } from "../core/textMetricProfile.js";
|
|
2
33
|
import { isPlainObject } from "../core/immutable.js";
|
|
3
34
|
import { validateUserId } from "../core/identifiers.js";
|
|
@@ -130,6 +161,12 @@ export function validateProgramState(program, path) {
|
|
|
130
161
|
}
|
|
131
162
|
resources.set(referenceKind, entries);
|
|
132
163
|
for (const resource of entries.values()) {
|
|
164
|
+
if (kind === "scale") {
|
|
165
|
+
normalizeColorInterpolationOptions(resource.interpolate, resource);
|
|
166
|
+
if (resource.type === "pow") normalizeTransformParameters("pow", {
|
|
167
|
+
...(resource.exponent === undefined ? {} : {exponent:resource.exponent})
|
|
168
|
+
});
|
|
169
|
+
}
|
|
133
170
|
for (const [key, value] of Object.entries(resource)) {
|
|
134
171
|
if (key !== "id") validateSemanticProperties(program, value, `${kind}[${resource.id}].${key}`);
|
|
135
172
|
}
|
|
@@ -141,7 +178,178 @@ export function validateProgramState(program, path) {
|
|
|
141
178
|
}
|
|
142
179
|
for (const key of ["resolvedScales", "materializationConfigs", "context"]) requireObject(program[key], `${path}.${key}`);
|
|
143
180
|
for (const key of ["marks", "guides"]) requireObject(program.materializationConfigs[key], `${path}.materializationConfigs.${key}`);
|
|
181
|
+
if (program.compositionSpec?.type === "facet" && isPlainObject(program.compositionSpec.facet.guides.legend)) {
|
|
182
|
+
validateAuthoredFacetLegend(program, program.compositionSpec.facet.guides.legend);
|
|
183
|
+
}
|
|
144
184
|
if (program.materializationConfigs.textMetrics !== undefined) normalizeTextMetricProfile(program.materializationConfigs.textMetrics);
|
|
185
|
+
for (const config of [
|
|
186
|
+
...Object.values(program.guideConfigs.axis ?? {}).map(axis => axis.ticks),
|
|
187
|
+
...["horizontal", "vertical"].map(direction => program.guideConfigs.grid?.[direction])
|
|
188
|
+
]) {
|
|
189
|
+
if (config?.strokeDash !== undefined) validateGuideDash(config.strokeDash);
|
|
190
|
+
if (config?.strokeDashMap !== undefined) normalizeGuideDashMap(config.strokeDashMap, { reset: false });
|
|
191
|
+
}
|
|
192
|
+
for (const owner of cartesianAxisOwners(program)) {
|
|
193
|
+
const channel = owner.id;
|
|
194
|
+
const axis = owner.config;
|
|
195
|
+
if (axis?.frame !== undefined) {
|
|
196
|
+
exactKeys(axis.frame, ["offset"], `${path}.guides.axis.${channel}.frame`);
|
|
197
|
+
validateNonNegativeFinite(axis.frame.offset, "Axis frame offset");
|
|
198
|
+
}
|
|
199
|
+
if (axis?.labels !== undefined) validateAxisTextPlacement(axis.labels);
|
|
200
|
+
if (axis?.title !== undefined) validateAxisTextPlacement(axis.title, { title: true });
|
|
201
|
+
}
|
|
202
|
+
for (const id of Object.keys(program.guideConfigs.axis ?? {})) {
|
|
203
|
+
if (!AXIS_RESOURCE_CHANNELS.includes(id) && !Object.hasOwn(program.semanticSpec.guides.axis ?? {}, id)) {
|
|
204
|
+
throw invalidSnapshot("Named axis configs require their semantic owner.", path);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
for (const [id, semanticAxis] of Object.entries(program.semanticSpec.guides.axis ?? {})) {
|
|
208
|
+
if (AXIS_RESOURCE_CHANNELS.includes(id)) continue;
|
|
209
|
+
validateUserId(id, "Axis id");
|
|
210
|
+
validateKeys(semanticAxis, ["channel", "position", "scale", "coordinate", "title"], "Stored named axis");
|
|
211
|
+
if (!["x", "y"].includes(semanticAxis.channel) ||
|
|
212
|
+
!(semanticAxis.channel === "x" ? ["bottom", "top"] : ["left", "right"]).includes(semanticAxis.position)) {
|
|
213
|
+
throw invalidSnapshot("Named axes require a Cartesian channel and compatible physical side.", path);
|
|
214
|
+
}
|
|
215
|
+
validateUserId(semanticAxis.scale, "Axis scale id");
|
|
216
|
+
validateUserId(semanticAxis.coordinate, "Axis coordinate id");
|
|
217
|
+
if (resources.get("coordinate").get(semanticAxis.coordinate)?.type !== "cartesian") {
|
|
218
|
+
throw invalidSnapshot("Named Cartesian axes require a Cartesian coordinate.", path);
|
|
219
|
+
}
|
|
220
|
+
const config = program.guideConfigs.axis?.[id];
|
|
221
|
+
requireObject(config, `${path}.guides.axis.${id}`);
|
|
222
|
+
validateKeys(config, ["binding", "frame", "line", "ticks", "labels", "title"], "Stored named axis config");
|
|
223
|
+
const components = ["line", "ticks", "labels", "title"];
|
|
224
|
+
if (!components.some(component => config[component] !== undefined)) {
|
|
225
|
+
throw invalidSnapshot("Named axes require at least one retained component.", path);
|
|
226
|
+
}
|
|
227
|
+
for (const [index, component] of components.entries()) {
|
|
228
|
+
const stored = config[component];
|
|
229
|
+
const graphic = program.graphicSpec.objects[axisGraphicIds(semanticAxis.channel, id)[index]];
|
|
230
|
+
if ((program.compositionSpec === undefined && (stored !== undefined) !== (graphic !== undefined)) ||
|
|
231
|
+
(stored !== undefined && (stored.position !== semanticAxis.position ||
|
|
232
|
+
stored.scale !== undefined && stored.scale !== semanticAxis.scale))) {
|
|
233
|
+
throw invalidSnapshot("Named axis components must share their owner's side and scale.", path);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if ((semanticAxis.title !== undefined) !== (config.title !== undefined)) {
|
|
237
|
+
throw invalidSnapshot("Named axis title meaning requires its retained title component.", path);
|
|
238
|
+
}
|
|
239
|
+
assertCartesianAxisSideAvailable(program, { id, channel: semanticAxis.channel,
|
|
240
|
+
coordinate: semanticAxis.coordinate, position: semanticAxis.position });
|
|
241
|
+
}
|
|
242
|
+
if (program.guideConfigs.legendGroups !== undefined) validateLegendGroupState(program.guideConfigs.legendGroups);
|
|
243
|
+
for (const [kind, config] of Object.entries(program.guideConfigs.legend ?? {})) {
|
|
244
|
+
validateLegendClip(kind,config);
|
|
245
|
+
if (localLegendPosition(config) !== undefined) {
|
|
246
|
+
const descriptor = describeLegendBlock(program, kind, config);
|
|
247
|
+
for (const [key, override] of Object.entries(config.blockOverrides)) {
|
|
248
|
+
if (override.position !== undefined && key !== descriptor.key) throw invalidSnapshot("Local position requires its existing block owner.", path);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
for(const layer of config.symbol?.layers??[])validateLegendLayerOpacity(layer.opacity);
|
|
252
|
+
if(kind==='colorSize') {
|
|
253
|
+
if(config.combine!=='color-size')throw invalidSnapshot('Color-size legend requires its combined owner.',path);
|
|
254
|
+
const layer=findLayer(program,config.target);
|
|
255
|
+
const mapping=resolveColorSizeMapping({layer,scales:program.resolvedScales,pointConfig:program.markConfigs[layer?.id],coordinate:findCoordinate(program,layer?.coordinate)});
|
|
256
|
+
for(const key of ['target','field','fieldType','colorScale','sizeScale','domain'])if(JSON.stringify(config[key])!==JSON.stringify(mapping[key]))throw invalidSnapshot('Stale color-size mapping.',path);
|
|
257
|
+
const guide=semantic.guides.legend?.colorSize;
|
|
258
|
+
if(guide?.colorScale!==mapping.colorScale||guide?.sizeScale!==mapping.sizeScale||guide.title!==config.title)throw invalidSnapshot('Stale color-size semantic owner.',path);
|
|
259
|
+
requireObject(config.symbol,path);normalizeColorSizeSymbol(config.symbol);
|
|
260
|
+
if(config.symbol.type!=='point'||!Object.hasOwn(config.symbol,'strokeWidth')||!Object.hasOwn(config.symbol,'opacity'))throw invalidSnapshot('Color-size symbol requires point type.',path);
|
|
261
|
+
const sample=config.sampling;requireObject(sample,path);
|
|
262
|
+
exactKeys(sample,sample.mode==='values'?['mode','count','values']:['mode','count'],path);
|
|
263
|
+
if(!['auto','values'].includes(sample.mode)||!Number.isInteger(sample.count)||sample.count<2)throw invalidSnapshot('Invalid color-size samples.',path);
|
|
264
|
+
validateGeneratedItemLimit(sample.count,'Color-size samples');
|
|
265
|
+
if(sample.mode==='values'){
|
|
266
|
+
requireArray(sample.values,path);
|
|
267
|
+
if(!sample.values.length||sample.values.length>100||sample.values.some((v,i)=>!Number.isFinite(v)||i>0&&v<=sample.values[i-1]||v<Math.min(...mapping.domain)||v>Math.max(...mapping.domain)||[mapping.colorScale,mapping.sizeScale].some(id=>program.resolvedScales[id].type==='log'&&v<=0)))throw invalidSnapshot('Invalid exact color-size samples.',path);
|
|
268
|
+
}
|
|
269
|
+
} else if (config.combine !== undefined) {
|
|
270
|
+
if (config.combine !== "size-shape" || kind !== "series" || !isSizeShapeChannels(config.channels)) throw invalidSnapshot("Invalid combined legend owner.", path);
|
|
271
|
+
const layer = semantic.layers.find(layer=>layer.id===config.target);
|
|
272
|
+
const definition = resolveSizeShapeMapping({layer,scales:program.resolvedScales,
|
|
273
|
+
order:semantic.guides.legend?.series?.order,rows:resolveDatasetReference(program,layer.data).values});
|
|
274
|
+
if (!Array.isArray(config.sizeAreas) || config.sizeAreas.length !== definition.sizeAreas.length ||
|
|
275
|
+
config.sizeAreas.some((area,index)=>!Object.is(area,definition.sizeAreas[index]))) throw invalidSnapshot("Stale combined legend size mapping.", path);
|
|
276
|
+
}
|
|
277
|
+
if (config.gradientDirection !== undefined) {
|
|
278
|
+
if (!["gradient", "strokeGradient"].includes(kind)) throw invalidSnapshot("Gradient direction requires a gradient owner.", path);
|
|
279
|
+
normalizeGradientDirection(config.gradientDirection, "Stored gradient direction");
|
|
280
|
+
}
|
|
281
|
+
if (["gradient", "strokeGradient"].includes(kind) && config.representation !== "symbols" && !["top", "bottom"].includes(config.titlePosition)) {
|
|
282
|
+
throw invalidSnapshot("Gradient titlePosition requires top or bottom.", path);
|
|
283
|
+
}
|
|
284
|
+
if (config.border && Object.hasOwn(config.border, "strokeDash")) {
|
|
285
|
+
normalizeLegendBorderDash(config.border.strokeDash, "Stored legend border strokeDash");
|
|
286
|
+
}
|
|
287
|
+
if (["size","colorSize"].includes(kind) && config.blockOverrides !== undefined) {
|
|
288
|
+
for (const [key, override] of Object.entries(config.blockOverrides)) {
|
|
289
|
+
if (override.labelMap === undefined) continue;
|
|
290
|
+
if (key !== (kind==='colorSize'?'["color","size"]':'["size"]') || !isContinuousSizeScaleType(program.resolvedScales[kind==='colorSize'?config.sizeScale:config.scale]?.type)) {
|
|
291
|
+
throw invalidSnapshot("Numeric display labels require a continuous size legend block.", path);
|
|
292
|
+
}
|
|
293
|
+
normalizeNumericDisplayLabelMap(override.labelMap, "Stored size legend labelMap");
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (config.titleStyle !== undefined) {
|
|
297
|
+
validateOptionObject(config.titleStyle, LEGEND_TITLE_STYLE_KEYS, "Stored legend title style");
|
|
298
|
+
validateLegendTitleAlignment(config.titleStyle, config.titlePosition);
|
|
299
|
+
}
|
|
300
|
+
if (config.inferredOffset !== undefined && typeof config.inferredOffset !== "boolean") throw invalidSnapshot("Invalid legend offset inference.", path);
|
|
301
|
+
if (["gradient", "strokeGradient"].includes(kind) && config.representation !== "symbols") {
|
|
302
|
+
validateLegendGradient(config.gradient, "Stored legend gradient");
|
|
303
|
+
}
|
|
304
|
+
if (config.representation === undefined) continue;
|
|
305
|
+
if (kind !== "gradient" || !["symbols", "gradient"].includes(config.representation)) throw invalidSnapshot("Invalid continuous color legend representation.", path);
|
|
306
|
+
if (config.representation === "symbols") {
|
|
307
|
+
if (config.gradient !== undefined) throw invalidSnapshot("Symbol color legend cannot store gradient dimensions.", path);
|
|
308
|
+
requireObject(config.sampling, path);
|
|
309
|
+
const sample = config.sampling;
|
|
310
|
+
exactKeys(sample, sample.mode === "values" ? ["mode", "values", "count"] : ["mode", "count"], path);
|
|
311
|
+
if (!["auto", "values"].includes(sample.mode)) throw invalidSnapshot("Invalid color legend sample mode.", path);
|
|
312
|
+
if (!Number.isInteger(sample.count) || sample.count < 2) throw invalidSnapshot("Invalid color legend sample count.", path);
|
|
313
|
+
validateGeneratedItemLimit(sample.count, "Color legend sample count");
|
|
314
|
+
const scale = program.resolvedScales[config.scale];
|
|
315
|
+
if (!isContinuousColorScaleType(scale?.type)) throw invalidSnapshot("Sampled color legend requires a continuous color scale.", path);
|
|
316
|
+
if (sample.mode === "values") {
|
|
317
|
+
requireArray(sample.values, path);
|
|
318
|
+
if (!sample.values.length || sample.values.length > 100 || sample.values.some((value, index) => !Number.isFinite(value) || index > 0 && value <= sample.values[index - 1] || value < Math.min(...scale.domain) || value > Math.max(...scale.domain) || scale.type === "log" && value <= 0)) throw invalidSnapshot("Invalid exact color legend samples.", path);
|
|
319
|
+
}
|
|
320
|
+
requireObject(config.symbol, path);
|
|
321
|
+
validateKeys(config.symbol, ["type", "radius", "stroke", "strokeWidth"], "Stored sampled color symbol");
|
|
322
|
+
if (config.symbol.type !== "point") throw invalidSnapshot("Invalid sampled color symbol type.", path);
|
|
323
|
+
validatePositiveFinite(config.symbol.radius, "Stored sampled color radius");
|
|
324
|
+
validateNonNegativeFinite(config.symbol.strokeWidth, "Stored sampled color strokeWidth");
|
|
325
|
+
if (config.symbol.stroke !== undefined) validateNonEmptyString(config.symbol.stroke, "Stored sampled color stroke");
|
|
326
|
+
if (!["left", "right", "top", "bottom", "top-left", "top-right", "bottom-left", "bottom-right"].includes(config.position) || !["horizontal", "vertical"].includes(config.direction) || config.layout !== "edge" || !["left", "center", "right"].includes(config.align) || !["top", "left"].includes(config.titlePosition) || config.columns !== undefined && (!Number.isInteger(config.columns) || config.columns < 1)) throw invalidSnapshot("Invalid sampled color item layout.", path);
|
|
327
|
+
validatePositiveFinite(config.itemGap, "Stored sampled color itemGap");
|
|
328
|
+
validateNonNegativeFinite(config.offset, "Stored sampled color offset");
|
|
329
|
+
} else if (["sampling", "symbol", "columns", "direction", "layout"].some(key => config[key] !== undefined)) throw invalidSnapshot("Gradient color legend cannot store symbol representation settings.", path);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const regressionFits = program.materializationConfigs.regressionFits ?? {};
|
|
333
|
+
requireObject(regressionFits, `${path}.materializationConfigs.regressionFits`);
|
|
334
|
+
for (const [id, cache] of Object.entries(regressionFits)) {
|
|
335
|
+
const dataset = resources.get("data").get(id);
|
|
336
|
+
validateRegressionFitStatistics(cache, dataset, resources.get("data").get(dataset?.source));
|
|
337
|
+
}
|
|
338
|
+
for (const dataset of semantic.datasets) {
|
|
339
|
+
if (dataset.transform?.[0]?.type !== "regressionStatistics") continue;
|
|
340
|
+
const fit = resources.get("data").get(dataset.source);
|
|
341
|
+
requireRegressionStatisticsSource(fit);
|
|
342
|
+
if (regressionFits[fit.id] === undefined) throw invalidSnapshot("Regression statistics requires its linear fit cache.", path);
|
|
343
|
+
if (dataset.values !== undefined) {
|
|
344
|
+
const expected = regressionStatisticsRows(regressionFits[fit.id], fit);
|
|
345
|
+
if (dataset.values.length !== expected.length || dataset.values.some((row, index) =>
|
|
346
|
+
!isPlainObject(row) || Object.keys(row).length !== Object.keys(expected[index]).length ||
|
|
347
|
+
Object.entries(expected[index]).some(([key, value]) => !Object.hasOwn(row, key) || !Object.is(row[key], value)))) {
|
|
348
|
+
throw invalidSnapshot("Regression statistics values do not match their linear fit cache.", path);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
145
353
|
const selections = program.materializationConfigs.selections ?? {};
|
|
146
354
|
requireObject(selections, `${path}.materializationConfigs.selections`);
|
|
147
355
|
for (const [id, selection] of Object.entries(selections)) {
|
|
@@ -161,12 +369,19 @@ export function validateProgramState(program, path) {
|
|
|
161
369
|
requireObject(scale, `${path}.resolvedScales.${id}`);
|
|
162
370
|
requireArray(scale.domain, `${path}.resolvedScales.${id}.domain`);
|
|
163
371
|
requireArray(scale.range, `${path}.resolvedScales.${id}.range`);
|
|
372
|
+
normalizeColorInterpolationOptions(scale.interpolate, scale);
|
|
373
|
+
if (scale.type === "pow") normalizeTransformParameters("pow", {
|
|
374
|
+
...(scale.exponent === undefined ? {} : {exponent:scale.exponent})
|
|
375
|
+
});
|
|
164
376
|
if (!resources.get("scale").has(id) || scale.type !== resources.get("scale").get(id).type) {
|
|
165
377
|
throw invalidSnapshot(`Resolved scale "${id}" has no matching semantic owner/type.`, path);
|
|
166
378
|
}
|
|
167
379
|
}
|
|
168
380
|
for (const id of Object.keys(program.markConfigs)) {
|
|
169
381
|
if (!resources.get("mark").has(id)) throw invalidSnapshot(`Mark config "${id}" has no layer owner.`, path);
|
|
382
|
+
if (resources.get("mark").get(id).mark.type === "area") {
|
|
383
|
+
validateAreaPaintConfig(program.markConfigs[id]);
|
|
384
|
+
}
|
|
170
385
|
}
|
|
171
386
|
for (const kind of ["data", "mark"]) {
|
|
172
387
|
const visited = new Set();
|
|
@@ -182,4 +397,9 @@ export function validateProgramState(program, path) {
|
|
|
182
397
|
for (const resource of resources.get(kind).values()) visit(resource);
|
|
183
398
|
}
|
|
184
399
|
validateGraphic(program.graphicSpec, `${path}.graphicSpec`);
|
|
400
|
+
for (const [id, graphic] of Object.entries(program.graphicSpec.objects)) {
|
|
401
|
+
if (graphic.items !== undefined) validateGraphicItemLimit(graphic.items.length, resolveGraphicItemCapacity(program, {
|
|
402
|
+
id, label: "Graphic collection length"
|
|
403
|
+
}));
|
|
404
|
+
}
|
|
185
405
|
}
|