ggaction 0.0.16 → 0.0.18
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 +66 -0
- package/README.md +3 -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 +10 -4
- 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 +16 -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 +4 -4
- 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/bin.js +1 -1
- package/src/actions/data/box.js +2 -1
- package/src/actions/data/create.js +18 -5
- package/src/actions/data/density.js +1 -1
- package/src/actions/data/derived.js +15 -1
- package/src/actions/data/edit.js +20 -7
- package/src/actions/data/filter.js +15 -4
- package/src/actions/data/index.js +3 -0
- package/src/actions/data/interval.js +2 -0
- package/src/actions/data/regression.js +6 -2
- package/src/actions/data/revise.js +37 -5
- package/src/actions/data/shared.js +33 -4
- package/src/actions/data/sort.js +21 -0
- package/src/actions/data/stack.js +3 -1
- package/src/actions/data/summary.js +1 -1
- package/src/actions/encodings/angle.js +4 -1
- package/src/actions/encodings/appearance.js +43 -31
- package/src/actions/encodings/barWidth.js +36 -11
- package/src/actions/encodings/channels.js +0 -1
- package/src/actions/encodings/color/continuous.js +5 -2
- package/src/actions/encodings/color/index.js +5 -2
- package/src/actions/encodings/color/policy.js +10 -3
- package/src/actions/encodings/density/resolve.js +1 -6
- package/src/actions/encodings/offset.js +4 -1
- 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 +9 -3
- package/src/actions/encodings/ranged.js +23 -1
- package/src/actions/encodings/remove.js +3 -2
- package/src/actions/encodings/ruleAppearance.js +4 -1
- package/src/actions/encodings/seriesLayout.js +1 -0
- package/src/actions/encodings/stroke.js +4 -1
- package/src/actions/encodings/strokeDash.js +6 -1
- package/src/actions/encodings/text.js +12 -4
- package/src/actions/errorBands/create.js +2 -2
- package/src/actions/errorBands/edit.js +2 -1
- package/src/actions/facets/actions.js +158 -240
- package/src/actions/gradientPlots/resolve.js +2 -17
- package/src/actions/guides/applicability.js +1 -1
- package/src/actions/guides/axes/axis.js +16 -4
- package/src/actions/guides/axes/edit.js +4 -4
- package/src/actions/guides/axes/labels.js +120 -46
- package/src/actions/guides/axes/lines.js +6 -5
- 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 +3 -2
- package/src/actions/guides/axes/tickGroups.js +5 -2
- package/src/actions/guides/axes/ticks.js +6 -3
- package/src/actions/guides/axes/titles.js +71 -26
- package/src/actions/guides/basic.js +2 -0
- package/src/actions/guides/facadeLegend.js +10 -2
- package/src/actions/guides/grids/grid.js +7 -1
- package/src/actions/guides/grids/resolve.js +59 -12
- package/src/actions/guides/legends/blocks.js +3 -1
- package/src/actions/guides/legends/categorical/actions.js +70 -17
- package/src/actions/guides/legends/categorical/components.js +12 -8
- package/src/actions/guides/legends/categorical/index.js +6 -0
- package/src/actions/guides/legends/categorical/layout.js +37 -8
- package/src/actions/guides/legends/categorical/options.js +31 -20
- package/src/actions/guides/legends/categorical/resolve.js +2 -2
- package/src/actions/guides/legends/categorical/symbols.js +18 -12
- package/src/actions/guides/legends/categorical/validation.js +1 -6
- 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 +47 -34
- package/src/actions/guides/legends/continuous/interval.js +4 -4
- package/src/actions/guides/legends/continuous/opacity.js +19 -12
- package/src/actions/guides/legends/edit.js +1 -604
- package/src/actions/guides/legends/focused.js +2 -2
- package/src/actions/guides/legends/group.js +1 -0
- package/src/actions/guides/legends/lane.js +1 -516
- package/src/actions/guides/legends/remove.js +3 -0
- package/src/actions/guides/legends/sampling.js +1 -1
- package/src/actions/guides/legends/size.js +51 -43
- package/src/actions/guides/legends/strokeWidth.js +11 -29
- package/src/actions/guides/legends/target.js +3 -3
- package/src/actions/guides/legends/transition.js +21 -7
- 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/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 +17 -4
- package/src/actions/marks/bar/edit.js +18 -7
- package/src/actions/marks/bar/materialize.js +9 -6
- package/src/actions/marks/line/actions.js +11 -4
- package/src/actions/marks/line/materialize.js +9 -5
- package/src/actions/marks/point/create.js +8 -1
- package/src/actions/marks/point/edit.js +13 -6
- package/src/actions/marks/point/materialize.js +4 -2
- package/src/actions/marks/rect/actions.js +26 -11
- package/src/actions/marks/remove.js +2 -0
- package/src/actions/marks/rule/actions.js +31 -12
- package/src/actions/marks/text/actions.js +42 -11
- package/src/actions/marks/text/layout.js +3 -0
- package/src/actions/marks/tick/actions.js +23 -8
- package/src/actions/primitives/semanticAction.js +37 -3
- package/src/actions/primitives/semanticValidation/dataset.js +13 -1
- package/src/actions/primitives/semanticValidation/layer.js +26 -2
- package/src/actions/primitives/semanticValidation/scale.js +6 -0
- package/src/actions/regression/create.js +26 -5
- package/src/actions/regression/edit.js +50 -15
- package/src/actions/regression/follow.js +22 -0
- package/src/actions/regression/resolve.js +2 -6
- package/src/actions/scales/channels.js +20 -15
- package/src/actions/scales/consumers/common.js +2 -1
- package/src/actions/scales/consumers/families.js +16 -5
- package/src/actions/scales/create.js +10 -4
- package/src/actions/scales/definitions.js +1 -425
- package/src/actions/scales/edit.js +3 -3
- package/src/actions/scales/editPolicy.js +9 -3
- package/src/actions/scales/materialize.js +1 -1
- package/src/actions/scales/parallel.js +1 -1
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/preview.js +49 -2
- package/src/actions/scales/quantitativeColor.js +3 -3
- package/src/actions/selection/actions.js +1 -13
- package/src/actions/theme/reconcile.js +83 -27
- 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/action.js +3 -2
- package/src/core/compositionState.js +4 -1
- package/src/core/font.js +9 -2
- package/src/core/optionRequirements.js +1 -0
- package/src/core/programState.js +5 -1
- package/src/core/resourceReferences.js +3 -0
- package/src/core/textMetricProfile.js +3 -2
- package/src/core/textMetrics.js +41 -4
- package/src/core/validation.js +26 -0
- package/src/core/vocabulary.js +4 -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/bars/geometry.js +30 -3
- package/src/grammar/bars/policy.js +12 -1
- package/src/grammar/bin.js +41 -22
- package/src/grammar/bin2d.js +1 -6
- package/src/grammar/boxPlot.js +56 -9
- package/src/grammar/categoryOrder.js +1 -6
- package/src/grammar/complete.js +15 -19
- package/src/grammar/computed.js +92 -25
- package/src/grammar/curveCommands.js +29 -16
- package/src/grammar/datasetSchema.js +382 -0
- package/src/grammar/density.js +53 -5
- package/src/grammar/displayLabels.js +28 -0
- package/src/grammar/ecdf.js +2 -9
- package/src/grammar/facets/dependencies.js +6 -2
- package/src/grammar/facets/guides.js +5 -0
- package/src/grammar/facets/index.js +2 -5
- package/src/grammar/filter.js +133 -39
- package/src/grammar/fold.js +2 -12
- package/src/grammar/gradientProfile.js +1 -6
- package/src/grammar/impute.js +21 -48
- package/src/grammar/interval.js +34 -10
- package/src/grammar/itemMissing.js +59 -0
- package/src/grammar/legendGradient.js +18 -0
- package/src/grammar/legendGroups.js +1 -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/polar.js +1 -184
- package/src/grammar/positionCompatibility.js +3 -3
- package/src/grammar/regression/derive.js +63 -16
- package/src/grammar/regression/index.js +1 -0
- package/src/grammar/regression/models.js +116 -59
- package/src/grammar/regression/parameters.js +69 -6
- 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 -190
- 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 +11 -0
- package/src/grammar/schemas/graphic.js +3 -0
- package/src/grammar/schemas/semanticPath.js +5 -3
- package/src/grammar/sort.js +88 -0
- package/src/grammar/stack.js +2 -10
- package/src/grammar/statisticalReference.js +1 -6
- package/src/grammar/summary.js +120 -45
- package/src/grammar/text.js +19 -0
- package/src/grammar/timeUnit.js +16 -7
- package/src/grammar/transformKeys.js +47 -0
- package/src/grammar/transformTopology.js +1 -0
- package/src/grammar/transforms.js +25 -12
- package/src/grammar/valueFormat.js +17 -5
- package/src/grammar/weightedStatistics.js +1 -6
- package/src/grammar/window.js +4 -58
- package/src/inspection.js +495 -0
- package/src/inspectionDescriptors.js +3 -0
- package/src/layout/canvas.js +1 -110
- package/src/layout/facets.js +1 -244
- 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 +26 -3
- package/src/materialization/coordinateBounds.js +8 -0
- package/src/materialization/dependencies.js +4 -44
- package/src/materialization/facetGuides/layout.js +1 -0
- package/src/materialization/facetGuides/placement.js +19 -8
- package/src/materialization/facetGuides/preparation.js +15 -7
- package/src/materialization/facetHeaders.js +1 -347
- package/src/materialization/facets.js +1 -185
- package/src/materialization/graphicHierarchy.js +1 -1
- 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/legendGroups.js +1 -0
- package/src/materialization/legendText.js +1 -0
- package/src/materialization/legends.js +1 -1
- package/src/materialization/marks/capabilities.js +5 -6
- package/src/materialization/rect.js +1 -1
- package/src/materialization/revisionIdentity.js +19 -0
- package/src/materialization/scaleGuideDependencies.js +1 -1
- package/src/materialization/scales/map.js +3 -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 +72 -7
- 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 +17 -6
- package/src/materialization/textLines.js +15 -0
- package/src/persistence/codec.js +37 -6
- package/src/persistence/validation.js +47 -0
- package/src/persistence.js +94 -8
- package/src/renderers/canvas/circle.js +6 -3
- package/src/renderers/canvas/fill.js +7 -1
- package/src/renderers/canvas/index.js +2 -1
- 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/svg.js +4 -1
- 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 +1 -0
- package/types/index.d.ts +7 -0
- package/types/inspection.d.ts +17 -0
- package/types/program.d.ts +494 -112
|
@@ -7,14 +7,19 @@ import {
|
|
|
7
7
|
interpolateNumber,
|
|
8
8
|
inverseLerp
|
|
9
9
|
} from "../../../grammar/numeric.js";
|
|
10
|
-
export function resolveTemporalBarBand(valuesByConsumer, domain, range) {
|
|
10
|
+
export function resolveTemporalBarBand(valuesByConsumer, domain, range, markConfigs = {}, preserveRange = false) {
|
|
11
11
|
const temporalBars = valuesByConsumer.filter(({ consumer }) => {
|
|
12
12
|
const channels = resolveBarChannels(consumer.layer);
|
|
13
|
-
return resolveBarGrain(consumer.layer)
|
|
13
|
+
return [BAR_GRAINS.aggregate, BAR_GRAINS.ranged].includes(resolveBarGrain(consumer.layer)) &&
|
|
14
14
|
channels?.category === consumer.channel &&
|
|
15
15
|
consumer.encoding.fieldType === "temporal";
|
|
16
16
|
});
|
|
17
17
|
if (temporalBars.length === 0) return undefined;
|
|
18
|
+
const intervalBars = temporalBars.filter(({consumer}) => markConfigs[consumer.layer.id]?.barWidth?.interval !== undefined);
|
|
19
|
+
if (intervalBars.length > 0) {
|
|
20
|
+
if (intervalBars.length !== temporalBars.length) throw new Error("Temporal interval and centered bar widths cannot share a position scale.");
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
18
23
|
const temporalFields = new Set(
|
|
19
24
|
temporalBars.map(({ consumer }) => consumer.encoding.field)
|
|
20
25
|
);
|
|
@@ -31,6 +36,11 @@ export function resolveTemporalBarBand(valuesByConsumer, domain, range) {
|
|
|
31
36
|
"A temporal bar position scale requires compatible bar, line, rule, or text consumers of one field."
|
|
32
37
|
);
|
|
33
38
|
}
|
|
39
|
+
// Explicit pixel widths do not need an inferred half-slot inset. Keep one
|
|
40
|
+
// common range when a shared scale also has band-width bar consumers.
|
|
41
|
+
preserveRange ||= temporalBars.every(({ consumer }) =>
|
|
42
|
+
markConfigs[consumer.layer.id]?.barWidth?.pixels !== undefined
|
|
43
|
+
);
|
|
34
44
|
const values = temporalBars.flatMap(item => item.values);
|
|
35
45
|
const ordered = [...new Set(values)].sort((left, right) => left - right);
|
|
36
46
|
if (ordered.length < 2) {
|
|
@@ -59,7 +69,7 @@ export function resolveTemporalBarBand(valuesByConsumer, domain, range) {
|
|
|
59
69
|
interpolateNumber(range[0], range[1], bandwidthFraction) - range[0]
|
|
60
70
|
)
|
|
61
71
|
: directBandwidth;
|
|
62
|
-
const resolvedRange = stable
|
|
72
|
+
const resolvedRange = preserveRange ? [...range] : stable
|
|
63
73
|
? [
|
|
64
74
|
interpolateNumber(range[0], range[1], bandwidthFraction / 2),
|
|
65
75
|
interpolateNumber(range[0], range[1], 1 - bandwidthFraction / 2)
|
|
@@ -79,10 +89,12 @@ export function resolveTemporalBarBand(valuesByConsumer, domain, range) {
|
|
|
79
89
|
});
|
|
80
90
|
let bandwidth = Infinity;
|
|
81
91
|
for (let index = 1; index < positions.length; index += 1) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
92
|
+
const gap = Math.abs(positions[index] - positions[index - 1]);
|
|
93
|
+
// Keep a finite conservative slot when an explicitly requested span itself
|
|
94
|
+
// exceeds the representable difference of two finite endpoints.
|
|
95
|
+
bandwidth = Math.min(bandwidth,
|
|
96
|
+
preserveRange && !Number.isFinite(gap) ? estimatedBandwidth : gap);
|
|
97
|
+
|
|
86
98
|
}
|
|
87
99
|
if (![estimatedBandwidth, bandwidth, ...resolvedRange].every(Number.isFinite)) {
|
|
88
100
|
throw new RangeError("Temporal bar position exceeds the finite numeric range.");
|
|
@@ -27,11 +27,13 @@ export function resolveSeriesLayoutDomain({
|
|
|
27
27
|
seriesLayouts[index] === undefined &&
|
|
28
28
|
!isSourceOwnedText(consumer.layer)
|
|
29
29
|
);
|
|
30
|
+
const layout = activeLayouts[0].layout;
|
|
30
31
|
const compatibleDirect = directConsumers.every(({ consumer }) =>
|
|
31
32
|
layoutConsumers.every(({ consumer: layoutConsumer }) => {
|
|
32
33
|
const aggregate = layoutConsumer.encoding.aggregate;
|
|
33
34
|
const directEncoding = consumer.encoding;
|
|
34
35
|
return directEncoding.fieldType === "quantitative" && (
|
|
36
|
+
layout === "overlay" ||
|
|
35
37
|
Object.hasOwn(directEncoding, "datum") ||
|
|
36
38
|
(
|
|
37
39
|
aggregate !== "count" &&
|
|
@@ -45,7 +47,6 @@ export function resolveSeriesLayoutDomain({
|
|
|
45
47
|
`Series layout scale "${id}" cannot be shared with another policy.`
|
|
46
48
|
);
|
|
47
49
|
}
|
|
48
|
-
const layout = activeLayouts[0].layout;
|
|
49
50
|
const values = [
|
|
50
51
|
...activeLayouts.flatMap(item => item.values),
|
|
51
52
|
...directConsumers.flatMap(item => item.values)
|
|
@@ -35,6 +35,36 @@ import { resolveOffsetScalePolicy } from "./policies/offset.js";
|
|
|
35
35
|
import { resolveBinnedPositionDomain } from "./policies/binnedPosition.js";
|
|
36
36
|
import { resolveSeriesLayoutDomain } from "./policies/series.js";
|
|
37
37
|
import { OFFSET_POSITION_CHANNELS } from "../../core/vocabulary.js";
|
|
38
|
+
import { annotateError } from "../../core/diagnostics.js";
|
|
39
|
+
import { cloneAndFreeze } from "../../core/immutable.js";
|
|
40
|
+
import { comparableDataIdentity } from "../revisionIdentity.js";
|
|
41
|
+
|
|
42
|
+
const bindingSignatures = new WeakMap();
|
|
43
|
+
|
|
44
|
+
function scaleBindingSignature(channel, consumers, { comparable = false } = {}) {
|
|
45
|
+
return consumers.map(consumer => ({
|
|
46
|
+
channel: consumer.channel ?? channel,
|
|
47
|
+
coordinate: consumer.layer.coordinate,
|
|
48
|
+
data: comparable
|
|
49
|
+
? comparableDataIdentity(consumer.layer.data)
|
|
50
|
+
: consumer.layer.data,
|
|
51
|
+
field: consumer.encoding.field,
|
|
52
|
+
datum: consumer.encoding.datum,
|
|
53
|
+
fieldType: consumer.encoding.fieldType,
|
|
54
|
+
temporalUnit: consumer.encoding.temporalUnit,
|
|
55
|
+
aggregate: consumer.encoding.aggregate,
|
|
56
|
+
bin: consumer.encoding.bin
|
|
57
|
+
})).sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function rememberResolvedScaleBinding(resolvedScale, channel, consumers) {
|
|
61
|
+
if (resolvedScale !== undefined) {
|
|
62
|
+
bindingSignatures.set(
|
|
63
|
+
resolvedScale,
|
|
64
|
+
cloneAndFreeze(scaleBindingSignature(channel, consumers))
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
38
68
|
|
|
39
69
|
function resolveDefaultDomain({
|
|
40
70
|
scale,
|
|
@@ -130,7 +160,7 @@ function resolveContinuousScale({
|
|
|
130
160
|
}) {
|
|
131
161
|
return {
|
|
132
162
|
type: isSequentialColor
|
|
133
|
-
?
|
|
163
|
+
? scale.type
|
|
134
164
|
: isOrdinalAppearance
|
|
135
165
|
? validateOrdinalScaleType(scale.type)
|
|
136
166
|
: scale.type === "time"
|
|
@@ -206,12 +236,22 @@ export function resolveScaleMaterialization(options) {
|
|
|
206
236
|
.flatMap(item => item.values)
|
|
207
237
|
.filter(value => value !== undefined);
|
|
208
238
|
const preserved = resolvedScales[id]?.domain;
|
|
239
|
+
const bindingSignature = scaleBindingSignature(channel, consumers);
|
|
240
|
+
const comparableBindingSignature = scaleBindingSignature(channel, consumers, {
|
|
241
|
+
comparable: true
|
|
242
|
+
});
|
|
243
|
+
const priorSignature = resolvedScales[id] === undefined
|
|
244
|
+
? undefined
|
|
245
|
+
: bindingSignatures.get(resolvedScales[id]);
|
|
246
|
+
const samePreservedMeaning = priorSignature !== undefined &&
|
|
247
|
+
JSON.stringify(priorSignature) === JSON.stringify(comparableBindingSignature);
|
|
209
248
|
if (
|
|
210
249
|
scale.domain === "auto" &&
|
|
211
250
|
Array.isArray(preserved) &&
|
|
212
|
-
|
|
251
|
+
resolvedScales[id]?.type === scale.type &&
|
|
252
|
+
(consumers.some(({ layer }) =>
|
|
213
253
|
markConfigs?.[layer.id]?.markFilter?.empty === true
|
|
214
|
-
)
|
|
254
|
+
) || (scale.emptyDomain === "preserve" && allValues.length === 0 && samePreservedMeaning))
|
|
215
255
|
) {
|
|
216
256
|
return resolveScaleMaterialization({
|
|
217
257
|
...options,
|
|
@@ -222,6 +262,27 @@ export function resolveScaleMaterialization(options) {
|
|
|
222
262
|
}))
|
|
223
263
|
});
|
|
224
264
|
}
|
|
265
|
+
if (
|
|
266
|
+
scale.domain === "auto" && allValues.length === 0 &&
|
|
267
|
+
scale.emptyDomain === "preserve" && Array.isArray(preserved) && !samePreservedMeaning
|
|
268
|
+
) {
|
|
269
|
+
throw annotateError(new Error(`Scale "${id}" requires an explicit domain because its binding changed.`), {
|
|
270
|
+
code: "incompatible-resource",
|
|
271
|
+
operation: "rematerializeScale",
|
|
272
|
+
optionPath: "domain",
|
|
273
|
+
resourceId: id,
|
|
274
|
+
reason: "domain-required"
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
if (scale.domain === "auto" && allValues.length === 0) {
|
|
278
|
+
throw annotateError(new Error(`Scale "${id}" requires an explicit domain for empty data.`), {
|
|
279
|
+
code: "incompatible-resource",
|
|
280
|
+
operation: "rematerializeScale",
|
|
281
|
+
optionPath: "domain",
|
|
282
|
+
resourceId: id,
|
|
283
|
+
reason: "domain-required"
|
|
284
|
+
});
|
|
285
|
+
}
|
|
225
286
|
if (channel === "size") {
|
|
226
287
|
return resolveSizeScale({ ...scale, values: allValues });
|
|
227
288
|
}
|
|
@@ -316,7 +377,7 @@ export function resolveScaleMaterialization(options) {
|
|
|
316
377
|
validateMeasuredRadiusConsumers({ scale, domain, range, consumers, markConfigs, thetaScales });
|
|
317
378
|
if (channel === "shape" && domain.length > range.length) {
|
|
318
379
|
throw new Error(
|
|
319
|
-
`Shape scale "${id}" requires at least one
|
|
380
|
+
`Shape scale "${id}" requires at least one shape entry per domain value.`
|
|
320
381
|
);
|
|
321
382
|
}
|
|
322
383
|
|
|
@@ -379,7 +440,9 @@ export function resolveScaleMaterialization(options) {
|
|
|
379
440
|
const temporalBarBand = resolveTemporalBarBand(
|
|
380
441
|
valuesByConsumer,
|
|
381
442
|
domain,
|
|
382
|
-
range
|
|
443
|
+
range,
|
|
444
|
+
markConfigs,
|
|
445
|
+
Array.isArray(scale.range)
|
|
383
446
|
);
|
|
384
447
|
if (temporalBarBand !== undefined) {
|
|
385
448
|
resolvedScale = {
|
|
@@ -389,7 +452,9 @@ export function resolveScaleMaterialization(options) {
|
|
|
389
452
|
};
|
|
390
453
|
}
|
|
391
454
|
}
|
|
392
|
-
|
|
455
|
+
const finalScale = cloneAndFreeze(scale.reverse === true
|
|
393
456
|
? reverseResolvedScale(resolvedScale)
|
|
394
|
-
: resolvedScale;
|
|
457
|
+
: resolvedScale);
|
|
458
|
+
bindingSignatures.set(finalScale, cloneAndFreeze(bindingSignature));
|
|
459
|
+
return finalScale;
|
|
395
460
|
}
|
|
@@ -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,10 +5,11 @@ 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";
|
|
12
|
+
import { applyItemMissingPolicy } from "../grammar/itemMissing.js";
|
|
12
13
|
import { findCoordinate } from "../selectors/coordinates.js";
|
|
13
14
|
import { findLayer } from "../selectors/layers.js";
|
|
14
15
|
import { unionConcreteGraphicBounds } from
|
|
@@ -273,12 +274,14 @@ function concreteTextItem(config, position, text, { offsets = true } = {}) {
|
|
|
273
274
|
if (text === undefined || !Number.isFinite(x) || !Number.isFinite(y)) {
|
|
274
275
|
return undefined;
|
|
275
276
|
}
|
|
277
|
+
const lines = resolveTextLines(text, config);
|
|
276
278
|
return {
|
|
277
279
|
type: "text",
|
|
278
280
|
properties: {
|
|
279
281
|
x: x + (offsets ? config.dx : 0),
|
|
280
282
|
y: y + (offsets ? config.dy : 0),
|
|
281
283
|
text,
|
|
284
|
+
...(lines === undefined ? {} : { lines }),
|
|
282
285
|
fill: config.fill,
|
|
283
286
|
opacity: config.opacity,
|
|
284
287
|
fontSize: config.fontSize,
|
|
@@ -309,6 +312,11 @@ function relativeLuminance(color) {
|
|
|
309
312
|
}
|
|
310
313
|
|
|
311
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
|
+
}
|
|
312
320
|
if (
|
|
313
321
|
!["arc", "rect"].includes(source.mark.type) ||
|
|
314
322
|
config.fillExplicit === true
|
|
@@ -382,6 +390,7 @@ function resolveSourceTextItems(program, layer, config) {
|
|
|
382
390
|
geometry: sourcePlacementGeometry(program, source, item),
|
|
383
391
|
text: {
|
|
384
392
|
text,
|
|
393
|
+
lines: resolveTextLines(text, resolvedConfig),
|
|
385
394
|
fontSize: resolvedConfig.fontSize,
|
|
386
395
|
fontFamily: resolvedConfig.fontFamily,
|
|
387
396
|
fontWeight: resolvedConfig.fontWeight,
|
|
@@ -395,7 +404,7 @@ function resolveSourceTextItems(program, layer, config) {
|
|
|
395
404
|
if (!resolved.visible) return [];
|
|
396
405
|
if (
|
|
397
406
|
resolved.anchor.startsWith("outside") &&
|
|
398
|
-
config.fillExplicit !== true
|
|
407
|
+
config.fillExplicit !== true && !config.inheritColor
|
|
399
408
|
) {
|
|
400
409
|
resolvedConfig = config;
|
|
401
410
|
}
|
|
@@ -414,11 +423,12 @@ function resolveSourceTextItems(program, layer, config) {
|
|
|
414
423
|
}
|
|
415
424
|
|
|
416
425
|
function resolveRowTextItems(program, layer, config) {
|
|
417
|
-
const
|
|
418
|
-
if (
|
|
426
|
+
const sourceDataset = findDataset(program, layer.data);
|
|
427
|
+
if (sourceDataset === undefined) {
|
|
419
428
|
throw new Error(`Text mark "${layer.id}" requires an existing dataset.`);
|
|
420
429
|
}
|
|
421
|
-
const
|
|
430
|
+
const dataset = applyItemMissingPolicy(layer, sourceDataset);
|
|
431
|
+
const encodings = [layer.encoding?.x, layer.encoding?.y, layer.encoding?.text, layer.encoding?.color];
|
|
422
432
|
const length = encodings.some(encoding => encoding !== undefined && Object.hasOwn(encoding, "field"))
|
|
423
433
|
? dataset.values.length
|
|
424
434
|
: 1;
|
|
@@ -452,9 +462,10 @@ function resolveRowTextItems(program, layer, config) {
|
|
|
452
462
|
const rows = length === 1 && dataset.values.length === 0
|
|
453
463
|
? [undefined]
|
|
454
464
|
: dataset.values.slice(0, length);
|
|
465
|
+
const fills = resolveRowEncodingValues(program, layer, dataset, "color");
|
|
455
466
|
return rows.flatMap((row, index) => {
|
|
456
467
|
const concrete = concreteItem(
|
|
457
|
-
config,
|
|
468
|
+
fills === undefined ? config : { ...config, fill: fills[index] },
|
|
458
469
|
{ x: x[index], y: y[index] },
|
|
459
470
|
contentValue(layer.encoding.text, row),
|
|
460
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
|
+
}
|
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,9 @@
|
|
|
1
|
+
import { LEGEND_TITLE_STYLE_KEYS, validateLegendTitleAlignment } from "../grammar/legendTitleStyle.js";
|
|
2
|
+
import { validateAreaPaintConfig } from "../grammar/areaPaint.js";
|
|
3
|
+
import { validateOptionObject, validateKeys, validatePositiveFinite, validateNonNegativeFinite, validateNonEmptyString, validateGeneratedItemLimit } from "../core/validation.js";
|
|
4
|
+
import { isContinuousColorScaleType } from "../grammar/scales/types.js";
|
|
5
|
+
import { validateLegendGroupState } from "../grammar/legendGroups.js";
|
|
6
|
+
import { validateLegendGradient } from "../grammar/legendGradient.js";
|
|
1
7
|
import { normalizeTextMetricProfile } from "../core/textMetricProfile.js";
|
|
2
8
|
import { isPlainObject } from "../core/immutable.js";
|
|
3
9
|
import { validateUserId } from "../core/identifiers.js";
|
|
@@ -142,6 +148,44 @@ export function validateProgramState(program, path) {
|
|
|
142
148
|
for (const key of ["resolvedScales", "materializationConfigs", "context"]) requireObject(program[key], `${path}.${key}`);
|
|
143
149
|
for (const key of ["marks", "guides"]) requireObject(program.materializationConfigs[key], `${path}.materializationConfigs.${key}`);
|
|
144
150
|
if (program.materializationConfigs.textMetrics !== undefined) normalizeTextMetricProfile(program.materializationConfigs.textMetrics);
|
|
151
|
+
if (program.guideConfigs.legendGroups !== undefined) validateLegendGroupState(program.guideConfigs.legendGroups);
|
|
152
|
+
for (const [kind, config] of Object.entries(program.guideConfigs.legend ?? {})) {
|
|
153
|
+
if (config.titleStyle !== undefined) {
|
|
154
|
+
validateOptionObject(config.titleStyle, LEGEND_TITLE_STYLE_KEYS, "Stored legend title style");
|
|
155
|
+
validateLegendTitleAlignment(config.titleStyle, config.titlePosition);
|
|
156
|
+
}
|
|
157
|
+
if (config.inferredOffset !== undefined && typeof config.inferredOffset !== "boolean") throw invalidSnapshot("Invalid legend offset inference.", path);
|
|
158
|
+
if (["gradient", "strokeGradient"].includes(kind) && config.representation !== "symbols") {
|
|
159
|
+
validateLegendGradient(config.gradient, "Stored legend gradient");
|
|
160
|
+
}
|
|
161
|
+
if (config.representation === undefined) continue;
|
|
162
|
+
if (kind !== "gradient" || !["symbols", "gradient"].includes(config.representation)) throw invalidSnapshot("Invalid continuous color legend representation.", path);
|
|
163
|
+
if (config.representation === "symbols") {
|
|
164
|
+
if (config.gradient !== undefined) throw invalidSnapshot("Symbol color legend cannot store gradient dimensions.", path);
|
|
165
|
+
requireObject(config.sampling, path);
|
|
166
|
+
const sample = config.sampling;
|
|
167
|
+
exactKeys(sample, sample.mode === "values" ? ["mode", "values", "count"] : ["mode", "count"], path);
|
|
168
|
+
if (!["auto", "values"].includes(sample.mode)) throw invalidSnapshot("Invalid color legend sample mode.", path);
|
|
169
|
+
if (!Number.isInteger(sample.count) || sample.count < 2) throw invalidSnapshot("Invalid color legend sample count.", path);
|
|
170
|
+
validateGeneratedItemLimit(sample.count, "Color legend sample count");
|
|
171
|
+
const scale = program.resolvedScales[config.scale];
|
|
172
|
+
if (!isContinuousColorScaleType(scale?.type)) throw invalidSnapshot("Sampled color legend requires a continuous color scale.", path);
|
|
173
|
+
if (sample.mode === "values") {
|
|
174
|
+
requireArray(sample.values, path);
|
|
175
|
+
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);
|
|
176
|
+
}
|
|
177
|
+
requireObject(config.symbol, path);
|
|
178
|
+
validateKeys(config.symbol, ["type", "radius", "stroke", "strokeWidth"], "Stored sampled color symbol");
|
|
179
|
+
if (config.symbol.type !== "point") throw invalidSnapshot("Invalid sampled color symbol type.", path);
|
|
180
|
+
validatePositiveFinite(config.symbol.radius, "Stored sampled color radius");
|
|
181
|
+
validateNonNegativeFinite(config.symbol.strokeWidth, "Stored sampled color strokeWidth");
|
|
182
|
+
if (config.symbol.stroke !== undefined) validateNonEmptyString(config.symbol.stroke, "Stored sampled color stroke");
|
|
183
|
+
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);
|
|
184
|
+
validatePositiveFinite(config.itemGap, "Stored sampled color itemGap");
|
|
185
|
+
validateNonNegativeFinite(config.offset, "Stored sampled color offset");
|
|
186
|
+
} else if (["sampling", "symbol", "columns", "direction", "layout"].some(key => config[key] !== undefined)) throw invalidSnapshot("Gradient color legend cannot store symbol representation settings.", path);
|
|
187
|
+
}
|
|
188
|
+
|
|
145
189
|
const selections = program.materializationConfigs.selections ?? {};
|
|
146
190
|
requireObject(selections, `${path}.materializationConfigs.selections`);
|
|
147
191
|
for (const [id, selection] of Object.entries(selections)) {
|
|
@@ -167,6 +211,9 @@ export function validateProgramState(program, path) {
|
|
|
167
211
|
}
|
|
168
212
|
for (const id of Object.keys(program.markConfigs)) {
|
|
169
213
|
if (!resources.get("mark").has(id)) throw invalidSnapshot(`Mark config "${id}" has no layer owner.`, path);
|
|
214
|
+
if (resources.get("mark").get(id).mark.type === "area") {
|
|
215
|
+
validateAreaPaintConfig(program.markConfigs[id]);
|
|
216
|
+
}
|
|
170
217
|
}
|
|
171
218
|
for (const kind of ["data", "mark"]) {
|
|
172
219
|
const visited = new Set();
|