ggaction 0.0.3 → 0.0.4

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.
Files changed (108) hide show
  1. package/CHANGELOG.md +28 -3
  2. package/README.md +95 -83
  3. package/package.json +11 -6
  4. package/src/actions/data/regression.js +1 -1
  5. package/src/actions/encodings/appearance.js +1 -1
  6. package/src/actions/encodings/color/continuous.js +137 -0
  7. package/src/actions/encodings/color/index.js +151 -0
  8. package/src/actions/encodings/color/layout.js +91 -0
  9. package/src/actions/encodings/color/policy.js +88 -0
  10. package/src/actions/encodings/index.js +2 -2
  11. package/src/actions/encodings/offset.js +1 -1
  12. package/src/actions/encodings/{position.js → position/index.js} +6 -6
  13. package/src/actions/encodings/position/resolve.js +1 -1
  14. package/src/actions/encodings/ranged.js +2 -2
  15. package/src/actions/encodings/strokeDash.js +1 -1
  16. package/src/actions/encodings/text.js +1 -1
  17. package/src/actions/errorBands/options.js +1 -1
  18. package/src/actions/errorBars/options.js +1 -1
  19. package/src/actions/facets/actions.js +1 -1
  20. package/src/actions/facets/derive.js +2 -2
  21. package/src/actions/facets/guides.js +1 -1
  22. package/src/actions/guides/axes/labels.js +1 -1
  23. package/src/actions/guides/axes/ticks.js +1 -1
  24. package/src/actions/guides/axes/titles.js +1 -1
  25. package/src/actions/guides/grids/resolve.js +1 -1
  26. package/src/actions/guides/legends/categorical/layout.js +4 -2
  27. package/src/actions/guides/legends/continuous/interval.js +1 -1
  28. package/src/actions/guides/legends/continuous/opacity.js +1 -1
  29. package/src/actions/guides/legends/edit.js +2 -20
  30. package/src/actions/guides/legends/remove.js +2 -20
  31. package/src/actions/guides/legends/size.js +1 -1
  32. package/src/actions/guides/legends/target.js +25 -0
  33. package/src/actions/guides/polar/{axes.js → axes/index.js} +5 -5
  34. package/src/actions/guides/polar/index.js +1 -1
  35. package/src/actions/guides/polar/resolve.js +1 -1
  36. package/src/actions/guides/tickValues.js +1 -1
  37. package/src/actions/intervals/resolve.js +1 -1
  38. package/src/actions/marks/arc/actions.js +2 -2
  39. package/src/actions/marks/area/actions.js +2 -2
  40. package/src/actions/marks/bar/edit.js +1 -1
  41. package/src/actions/marks/line/actions.js +3 -3
  42. package/src/actions/marks/rect/actions.js +1 -1
  43. package/src/actions/marks/rule/actions.js +1 -1
  44. package/src/actions/marks/shared.js +1 -1
  45. package/src/actions/marks/text/actions.js +1 -1
  46. package/src/actions/primitives/semanticValue.js +1 -1
  47. package/src/actions/regression/components.js +1 -1
  48. package/src/actions/regression/create.js +1 -1
  49. package/src/actions/regression/edit.js +1 -1
  50. package/src/actions/scales/consumers/common.js +91 -0
  51. package/src/actions/scales/consumers/families.js +47 -0
  52. package/src/actions/scales/consumers/index.js +41 -0
  53. package/src/actions/scales/{consumers.js → consumers/seriesLayout.js} +10 -132
  54. package/src/actions/scales/create.js +1 -1
  55. package/src/actions/scales/definitions.js +2 -2
  56. package/src/actions/scales/edit.js +3 -3
  57. package/src/actions/scales/materialize.js +2 -2
  58. package/src/core/ChartProgram.js +22 -236
  59. package/src/core/compositionState.js +169 -0
  60. package/src/core/materializationState.js +65 -0
  61. package/src/core/programState.js +28 -0
  62. package/src/grammar/arcs.js +1 -1
  63. package/src/grammar/areaSeries.js +1 -1
  64. package/src/grammar/bars/aggregate.js +1 -1
  65. package/src/grammar/density.js +1 -1
  66. package/src/grammar/{facets.js → facets/index.js} +5 -5
  67. package/src/grammar/interval.js +2 -96
  68. package/src/grammar/lineSeries.js +1 -1
  69. package/src/grammar/palettes.js +5 -7
  70. package/src/grammar/polarLineCommands.js +1 -1
  71. package/src/grammar/regression/derive.js +97 -0
  72. package/src/grammar/regression/index.js +22 -0
  73. package/src/grammar/regression/models.js +282 -0
  74. package/src/grammar/regression/parameters.js +106 -0
  75. package/src/grammar/rules.js +1 -1
  76. package/src/grammar/scales/color.js +4 -2
  77. package/src/grammar/statistics/studentT.js +98 -0
  78. package/src/grammar/transforms.js +1 -1
  79. package/src/materialization/bars/aggregate.js +1 -1
  80. package/src/materialization/bars/grouped.js +1 -1
  81. package/src/materialization/bars/histogram.js +1 -1
  82. package/src/materialization/bars/ranged.js +1 -1
  83. package/src/materialization/density.js +1 -1
  84. package/src/materialization/dependencies.js +6 -87
  85. package/src/materialization/encodings.js +2 -2
  86. package/src/materialization/facetGuides/index.js +2 -0
  87. package/src/materialization/facetGuides/legacyCategorical.js +125 -0
  88. package/src/materialization/facetGuides/placement.js +126 -0
  89. package/src/materialization/facetGuides/preparation.js +188 -0
  90. package/src/materialization/facets.js +1 -1
  91. package/src/materialization/marks/capabilities.js +141 -0
  92. package/src/materialization/marks/index.js +105 -0
  93. package/src/materialization/marks/policies.js +87 -0
  94. package/src/materialization/rect.js +1 -1
  95. package/src/materialization/rowEncoding.js +1 -1
  96. package/src/materialization/scaleGuideDependencies.js +94 -0
  97. package/src/materialization/scales/map.js +1 -1
  98. package/src/materialization/scales/policies/series.js +1 -1
  99. package/src/materialization/scales/resolve.js +1 -1
  100. package/src/materialization/selection/items/bar.js +1 -1
  101. package/src/materialization/selection/styles.js +1 -1
  102. package/src/renderers/canvas/text.js +14 -1
  103. package/types/extension.d.ts +4 -1
  104. package/src/actions/encodings/color.js +0 -424
  105. package/src/grammar/regression.js +0 -578
  106. package/src/grammar/scales.js +0 -1
  107. package/src/materialization/facetGuides.js +0 -426
  108. package/src/materialization/marks.js +0 -316
@@ -0,0 +1,188 @@
1
+ import {
2
+ unionConcreteGraphicBounds
3
+ } from "../../grammar/schemas/graphicBounds.js";
4
+ import {
5
+ resolveSharedFacetLegends
6
+ } from "../../grammar/facets/guides.js";
7
+ import { findLayer } from "../../selectors/layers.js";
8
+ import { findSemanticScale } from "../../selectors/scales.js";
9
+ import { allLegendGraphicIds } from "../guides/resources.js";
10
+ import { DEFAULT_COLORS } from "../../theme/defaults.js";
11
+ import { resolveLegacyCategoricalLegend } from "./legacyCategorical.js";
12
+
13
+ const LEGEND_SOURCE_CANVAS = Object.freeze({
14
+ width: 640,
15
+ height: 400,
16
+ margin: Object.freeze({ top: 40, right: 170, bottom: 60, left: 70 })
17
+ });
18
+
19
+ export function legendKinds(program) {
20
+ return Object.keys(program.guideConfigs.legend ?? {});
21
+ }
22
+
23
+ function legendCandidates(program) {
24
+ const layers = program.semanticSpec.layers;
25
+ const color = layers.filter(layer => {
26
+ const encoding = layer.encoding?.color;
27
+ return encoding?.scale !== undefined &&
28
+ findSemanticScale(program, encoding.scale) !== undefined;
29
+ });
30
+ if (color.length === 1) {
31
+ const layer = color[0];
32
+ return {
33
+ target: layer.id,
34
+ channels: ["color", "shape", "strokeDash"].filter(
35
+ channel => layer.encoding?.[channel]?.scale !== undefined
36
+ )
37
+ };
38
+ }
39
+ if (color.length > 1) {
40
+ throw new Error(
41
+ "Shared facet legend requires one unambiguous color legend target."
42
+ );
43
+ }
44
+ for (const channel of ["shape", "strokeDash", "size", "opacity"]) {
45
+ const candidates = layers.filter(
46
+ layer => layer.encoding?.[channel]?.scale !== undefined
47
+ );
48
+ if (candidates.length === 1) {
49
+ return { target: candidates[0].id, channels: [channel] };
50
+ }
51
+ if (candidates.length > 1) {
52
+ throw new Error(
53
+ `Shared facet legend requires one unambiguous ${channel} legend target.`
54
+ );
55
+ }
56
+ }
57
+ throw new Error("Shared facet legend requires one eligible encoded scale.");
58
+ }
59
+
60
+ function compactNumber(value) {
61
+ if (Math.abs(value) >= 1_000_000_000) {
62
+ return `${+(value / 1_000_000_000).toFixed(1)}B`;
63
+ }
64
+ if (Math.abs(value) >= 1_000_000) {
65
+ return `${Math.round(value / 1_000_000)}M`;
66
+ }
67
+ if (Math.abs(value) >= 1_000) {
68
+ return `${Math.round(value / 1_000)}K`;
69
+ }
70
+ return String(+value.toPrecision(3));
71
+ }
72
+
73
+ function continuousValues(domain, count) {
74
+ return Array.from(
75
+ { length: count },
76
+ (_, index) => domain[0] + index / (count - 1) * (domain[1] - domain[0])
77
+ );
78
+ }
79
+
80
+ function prepareAutoLegendSource(child) {
81
+ const request = legendCandidates(child);
82
+ const canvas = child.graphicSpec.objects.canvas;
83
+ let source = child
84
+ ._withCanvasConfig({
85
+ margin: LEGEND_SOURCE_CANVAS.margin,
86
+ size: { width: "explicit", height: "explicit" }
87
+ })
88
+ .editGraphics({
89
+ target: "canvas",
90
+ property: "width",
91
+ value: LEGEND_SOURCE_CANVAS.width
92
+ })
93
+ .editGraphics({
94
+ target: "canvas",
95
+ property: "height",
96
+ value: LEGEND_SOURCE_CANVAS.height
97
+ });
98
+ if (canvas?.properties.background !== undefined) {
99
+ source = source.editGraphics({
100
+ target: "canvas",
101
+ property: "background",
102
+ value: canvas.properties.background
103
+ });
104
+ }
105
+ const scale = findSemanticScale(
106
+ source,
107
+ findLayer(source, request.target)?.encoding?.color?.scale
108
+ );
109
+ const gradient = scale?.type === "sequential";
110
+ source = source.createLegend({
111
+ target: request.target,
112
+ channels: request.channels,
113
+ ...(gradient ? {
114
+ count: 5,
115
+ gradient: { length: 180, thickness: 12 },
116
+ labels: { offset: 9, fontSize: 10 },
117
+ titleStyle: { color: DEFAULT_COLORS.strongText, fontSize: 11 }
118
+ } : {})
119
+ });
120
+ if (gradient) {
121
+ const resolved = source.resolvedScales[scale.id];
122
+ source = source
123
+ .editGraphics({
124
+ target: "colorGradientLabels",
125
+ property: "text",
126
+ value: continuousValues(resolved.domain, 5).map(compactNumber)
127
+ })
128
+ .editGraphics({
129
+ target: "colorGradientTitle",
130
+ property: "y",
131
+ value: source.graphicSpec.objects.colorGradientTitle.properties.y + 8
132
+ });
133
+ }
134
+ return source;
135
+ }
136
+
137
+ function compatibleLegendChildren(program, source, inferred) {
138
+ return program.compositionSpec.children.map(id => {
139
+ const child = program.children[id];
140
+ return {
141
+ id,
142
+ guideConfigs: inferred ? source.guideConfigs : child.guideConfigs,
143
+ resolvedScales: child.resolvedScales
144
+ };
145
+ });
146
+ }
147
+
148
+ export function prepareSharedFacetLegend(program) {
149
+ if (program.compositionSpec.facet.guides.legend !== "shared") {
150
+ return undefined;
151
+ }
152
+ const firstId = program.compositionSpec.children[0];
153
+ const first = program.children[firstId];
154
+ const inferred = legendKinds(first).length === 0;
155
+ if (inferred) {
156
+ const legacy = resolveLegacyCategoricalLegend(program);
157
+ if (legacy !== undefined) return legacy;
158
+ }
159
+ const source = inferred ? prepareAutoLegendSource(first) : first;
160
+ const kinds = legendKinds(source);
161
+ const compatibility = resolveSharedFacetLegends(
162
+ compatibleLegendChildren(program, source, inferred)
163
+ );
164
+ const roots = allLegendGraphicIds(kinds).filter(
165
+ id => source.graphicSpec.objects[id] !== undefined
166
+ );
167
+ if (roots.length === 0) {
168
+ throw new Error("Shared facet legend has no concrete guide graphics.");
169
+ }
170
+ const bounds = unionConcreteGraphicBounds(source.graphicSpec, roots);
171
+ if (bounds === undefined) {
172
+ throw new Error("Shared facet legend has no measurable concrete bounds.");
173
+ }
174
+ const gradient = kinds.includes("gradient");
175
+ return {
176
+ mode: "promoted",
177
+ source,
178
+ inferred,
179
+ kinds,
180
+ roots,
181
+ bounds,
182
+ compatibility,
183
+ reservation: {
184
+ gap: gradient ? 24 : 18,
185
+ width: gradient ? 112 : Math.max(132, Math.ceil(bounds.right - bounds.left))
186
+ }
187
+ };
188
+ }
@@ -6,7 +6,7 @@ import {
6
6
  } from "../layout/title.js";
7
7
  import { resolveFacetLayout } from "../layout/facets.js";
8
8
  import { namespaceGraphicSnapshot } from "./compositionSnapshot.js";
9
- import { prepareSharedFacetLegend } from "./facetGuides.js";
9
+ import { prepareSharedFacetLegend } from "./facetGuides/index.js";
10
10
  import {
11
11
  attachSnapshotObject,
12
12
  clearCompositionChildren,
@@ -0,0 +1,141 @@
1
+ import { findDataset } from "../../selectors/datasets.js";
2
+ import { findLayer } from "../../selectors/layers.js";
3
+ import { isAggregate } from "../../grammar/aggregate.js";
4
+ import {
5
+ BAR_GRAINS,
6
+ resolveBarColorLayout,
7
+ resolveBarOffsetChannel,
8
+ resolveBarGrain
9
+ } from "../../grammar/bars/policy.js";
10
+ import { resolveRuleMode } from "../../grammar/rules.js";
11
+ import { findUpstreamTransform } from "../dataProvenance.js";
12
+ import { resolveRectMode } from "../../grammar/rects.js";
13
+
14
+ function hasCartesianPositionScales(layer) {
15
+ return (
16
+ layer.encoding?.x?.scale !== undefined &&
17
+ layer.encoding?.y?.scale !== undefined
18
+ );
19
+ }
20
+
21
+ function hasPolarPositionScales(layer) {
22
+ return (
23
+ layer.encoding?.theta?.scale !== undefined &&
24
+ layer.encoding?.radius?.scale !== undefined
25
+ );
26
+ }
27
+
28
+ export function canMaterializePoint(_program, layer) {
29
+ return layer.mark?.type === "point" && (
30
+ hasCartesianPositionScales(layer) || hasPolarPositionScales(layer)
31
+ );
32
+ }
33
+
34
+ export function canMaterializeLine(program, layer) {
35
+ const dataset = findDataset(program, layer.data);
36
+ const interval = dataset?.transform?.some(item => item.type === "interval");
37
+ if (hasPolarPositionScales(layer)) {
38
+ return (
39
+ layer.mark?.type === "line" &&
40
+ ["quantitative", "temporal", "ordinal", "nominal"].includes(
41
+ layer.encoding.theta.fieldType
42
+ ) &&
43
+ layer.encoding.radius.fieldType === "quantitative"
44
+ );
45
+ }
46
+ return (
47
+ layer.mark?.type === "line" &&
48
+ hasCartesianPositionScales(layer) &&
49
+ (isAggregate(layer.encoding.y.aggregate) ||
50
+ (layer.encoding.x.fieldType === "quantitative" &&
51
+ layer.encoding.y.fieldType === "quantitative") ||
52
+ ((interval || layer.encoding.y.aggregate === undefined) &&
53
+ ["quantitative", "temporal"].includes(layer.encoding.x.fieldType) &&
54
+ ["quantitative", "temporal"].includes(layer.encoding.y.fieldType) &&
55
+ layer.encoding.x.fieldType !== layer.encoding.y.fieldType))
56
+ );
57
+ }
58
+
59
+ export function canMaterializeArea(program, layer) {
60
+ if (layer.mark?.type !== "area" || !hasCartesianPositionScales(layer)) {
61
+ return false;
62
+ }
63
+ const dataset = findDataset(program, layer.data);
64
+ const densityTransform = findUpstreamTransform(program, dataset, "density");
65
+ const completeDensity =
66
+ densityTransform !== undefined &&
67
+ (densityTransform.groupBy === undefined ||
68
+ layer.encoding?.group?.field === densityTransform.groupBy);
69
+ return (
70
+ completeDensity ||
71
+ layer.encoding?.y2?.scale === layer.encoding.y.scale ||
72
+ layer.encoding?.x2?.scale === layer.encoding.x.scale
73
+ );
74
+ }
75
+
76
+ export function canMaterializeArc(_program, layer) {
77
+ if (
78
+ layer.mark?.type !== "arc" ||
79
+ layer.encoding?.theta?.scale === undefined ||
80
+ !["nominal", "ordinal"].includes(layer.encoding.theta.fieldType)
81
+ ) {
82
+ return false;
83
+ }
84
+ if (layer.encoding.theta.aggregate === "count") {
85
+ return layer.encoding?.radius === undefined;
86
+ }
87
+ return (
88
+ layer.encoding.theta.aggregate === undefined &&
89
+ layer.encoding?.radius?.scale !== undefined &&
90
+ layer.encoding.radius.fieldType === "quantitative"
91
+ );
92
+ }
93
+
94
+ export function canMaterializeBar(program, layer) {
95
+ if (layer.mark?.type !== "bar" || !hasCartesianPositionScales(layer)) {
96
+ return false;
97
+ }
98
+ const grain = resolveBarGrain(layer);
99
+ if (![BAR_GRAINS.histogram, BAR_GRAINS.aggregate, BAR_GRAINS.ranged].includes(grain)) {
100
+ return false;
101
+ }
102
+ if (
103
+ grain === BAR_GRAINS.aggregate &&
104
+ resolveBarColorLayout(layer) === "group"
105
+ ) {
106
+ const offsetChannel = resolveBarOffsetChannel(layer);
107
+ return (
108
+ layer.encoding?.color?.field !== undefined &&
109
+ layer.encoding?.[offsetChannel]?.field === layer.encoding.color.field &&
110
+ layer.encoding[offsetChannel].scale !== undefined
111
+ );
112
+ }
113
+ return true;
114
+ }
115
+
116
+ export function canMaterializeRule(program, layer) {
117
+ const fixedSpan = program.markConfigs[layer.id]?.fixedSpan;
118
+ const boxSpanOwner = program.markConfigs[layer.id]?.boxSpanOwner;
119
+ return layer.mark?.type === "rule" && (
120
+ resolveRuleMode(layer) !== undefined ||
121
+ (boxSpanOwner !== undefined && hasCartesianPositionScales(layer)) ||
122
+ (fixedSpan !== undefined && hasCartesianPositionScales(layer))
123
+ );
124
+ }
125
+
126
+ export function canMaterializeText(program, layer) {
127
+ if (layer.mark?.type !== "text" || layer.encoding?.text === undefined) {
128
+ return false;
129
+ }
130
+ if (layer.source !== undefined) {
131
+ const source = findLayer(program, layer.source);
132
+ return source !== undefined &&
133
+ ["point", "bar", "rule", "rect"].includes(source.mark?.type) &&
134
+ Array.isArray(program.graphicSpec.objects[source.id]?.items);
135
+ }
136
+ return hasCartesianPositionScales(layer);
137
+ }
138
+
139
+ export function canMaterializeRect(_program, layer) {
140
+ return resolveRectMode(layer) !== undefined;
141
+ }
@@ -0,0 +1,105 @@
1
+ import { POSITION_CHANNELS } from "../../core/vocabulary.js";
2
+ import { getMarkMaterializationPolicy } from "./policies.js";
3
+
4
+ export {
5
+ canMaterializeArc,
6
+ canMaterializeArea,
7
+ canMaterializeBar,
8
+ canMaterializeLine,
9
+ canMaterializePoint,
10
+ canMaterializeRect,
11
+ canMaterializeRule,
12
+ canMaterializeText
13
+ } from "./capabilities.js";
14
+
15
+ export function getMarkRematerializationStep(layer) {
16
+ const policy = getMarkMaterializationPolicy(layer);
17
+ return policy === undefined
18
+ ? undefined
19
+ : { op: policy.op, args: { id: layer.id } };
20
+ }
21
+
22
+ export function getMarkMaterializationStep(program, layer) {
23
+ const policy = getMarkMaterializationPolicy(layer);
24
+ if (policy === undefined || !policy.canMaterialize(program, layer)) {
25
+ return undefined;
26
+ }
27
+ return getMarkRematerializationStep(layer);
28
+ }
29
+
30
+ export function getSourceDependentMarkSteps(program, sourceId) {
31
+ return program.semanticSpec.layers.flatMap(layer =>
32
+ layer.source === sourceId &&
33
+ getMarkMaterializationPolicy(layer)?.sourceDependent === true
34
+ ? [getMarkMaterializationStep(program, layer)].filter(
35
+ step => step !== undefined
36
+ )
37
+ : []
38
+ );
39
+ }
40
+
41
+ export function getPositionEncodingMaterializationSteps(program, layer, scaleId) {
42
+ const policy = getMarkMaterializationPolicy(layer);
43
+ if (policy === undefined) return [];
44
+ const scale = { op: "rematerializeScale", args: { id: scaleId } };
45
+ const complete = policy.canMaterialize(program, layer);
46
+ const mark = getMarkRematerializationStep(layer);
47
+ if (!complete) {
48
+ if (policy.positionEncoding.incomplete === "scale") return [scale];
49
+ return policy.positionEncoding.scaleFirst ? [scale, mark] : [mark];
50
+ }
51
+ return policy.positionEncoding.scaleFirst ? [scale, mark] : [mark];
52
+ }
53
+
54
+ export function getScaleConsumerMaterializationMode(layer, channel) {
55
+ const policy = getMarkMaterializationPolicy(layer);
56
+ if (policy === undefined) return "direct";
57
+ if (POSITION_CHANNELS.includes(channel)) {
58
+ return policy.scaleApplication.position ?? policy.scaleApplication.default;
59
+ }
60
+ if (policy.scaleApplication.deferredChannels?.includes(channel)) {
61
+ return "defer";
62
+ }
63
+ return policy.scaleApplication.default;
64
+ }
65
+
66
+ export function canDeferScaleConsumerApplication(layer) {
67
+ return getMarkMaterializationPolicy(layer)?.scaleApplication.deferWithMark === true;
68
+ }
69
+
70
+ export function getExistingMarkRematerializationStep(program, layer) {
71
+ const policy = getMarkMaterializationPolicy(layer);
72
+ if (
73
+ policy?.rematerializeIncompleteExisting !== true ||
74
+ program.graphicSpec.objects[layer.id] === undefined
75
+ ) {
76
+ return undefined;
77
+ }
78
+ return getMarkRematerializationStep(layer);
79
+ }
80
+
81
+ export function getEncodingMaterializationStages(program, layer, channel, scale) {
82
+ const policy = getMarkMaterializationPolicy(layer);
83
+ if (policy === undefined) return { scales: [], marks: [] };
84
+ const scales = policy.encoding?.scaleFirst === true && scale !== undefined
85
+ ? [{
86
+ op: "rematerializeScale",
87
+ args: { id: scale, guides: false, marks: false }
88
+ }]
89
+ : [];
90
+ const shared = policy.encoding?.sharedChannels?.includes(channel) === true &&
91
+ scale !== undefined;
92
+ const candidates = shared
93
+ ? program.semanticSpec.layers.filter(candidate =>
94
+ candidate.mark?.type === layer.mark?.type &&
95
+ candidate.encoding?.[channel]?.scale === scale
96
+ )
97
+ : [layer];
98
+ const marks = candidates.flatMap(candidate => {
99
+ const step = policy.encoding?.completeOnly === true
100
+ ? getMarkMaterializationStep(program, candidate)
101
+ : getMarkRematerializationStep(candidate);
102
+ return step === undefined ? [] : [step];
103
+ });
104
+ return { scales, marks };
105
+ }
@@ -0,0 +1,87 @@
1
+ import {
2
+ canMaterializeArc,
3
+ canMaterializeArea,
4
+ canMaterializeBar,
5
+ canMaterializeLine,
6
+ canMaterializePoint,
7
+ canMaterializeRect,
8
+ canMaterializeRule,
9
+ canMaterializeText
10
+ } from "./capabilities.js";
11
+
12
+ const MARK_MATERIALIZATION_POLICIES = Object.freeze({
13
+ point: Object.freeze({
14
+ canMaterialize: canMaterializePoint,
15
+ op: "rematerializePointMark",
16
+ positionEncoding: Object.freeze({ incomplete: "mark", scaleFirst: true }),
17
+ encoding: Object.freeze({ scaleFirst: true }),
18
+ scaleApplication: Object.freeze({
19
+ deferWithMark: true,
20
+ position: "rematerialize",
21
+ deferredChannels: Object.freeze(["size", "shape"]),
22
+ default: "direct"
23
+ }),
24
+ rematerializeIncompleteExisting: true
25
+ }),
26
+ line: Object.freeze({
27
+ canMaterialize: canMaterializeLine,
28
+ op: "rematerializeLineMark",
29
+ positionEncoding: Object.freeze({ incomplete: "scale", scaleFirst: false }),
30
+ scaleApplication: Object.freeze({ default: "defer" })
31
+ }),
32
+ area: Object.freeze({
33
+ canMaterialize: canMaterializeArea,
34
+ op: "rematerializeAreaMark",
35
+ positionEncoding: Object.freeze({ incomplete: "scale", scaleFirst: true }),
36
+ encoding: Object.freeze({ sharedChannels: Object.freeze(["color"]) }),
37
+ scaleApplication: Object.freeze({ default: "defer" })
38
+ }),
39
+ arc: Object.freeze({
40
+ canMaterialize: canMaterializeArc,
41
+ op: "rematerializeArcMark",
42
+ positionEncoding: Object.freeze({ incomplete: "scale", scaleFirst: false }),
43
+ encoding: Object.freeze({ completeOnly: true }),
44
+ scaleApplication: Object.freeze({ default: "defer" })
45
+ }),
46
+ bar: Object.freeze({
47
+ canMaterialize: canMaterializeBar,
48
+ op: "rematerializeBarMark",
49
+ positionEncoding: Object.freeze({ incomplete: "scale", scaleFirst: false }),
50
+ scaleApplication: Object.freeze({ default: "defer" })
51
+ }),
52
+ rule: Object.freeze({
53
+ canMaterialize: canMaterializeRule,
54
+ op: "rematerializeRuleMark",
55
+ positionEncoding: Object.freeze({ incomplete: "mark", scaleFirst: false }),
56
+ scaleApplication: Object.freeze({ default: "defer" })
57
+ }),
58
+ text: Object.freeze({
59
+ canMaterialize: canMaterializeText,
60
+ op: "rematerializeTextMark",
61
+ positionEncoding: Object.freeze({ incomplete: "mark", scaleFirst: true }),
62
+ encoding: Object.freeze({ scaleFirst: true }),
63
+ scaleApplication: Object.freeze({
64
+ deferWithMark: true,
65
+ position: "rematerialize",
66
+ default: "defer"
67
+ }),
68
+ rematerializeIncompleteExisting: true,
69
+ sourceDependent: true
70
+ }),
71
+ rect: Object.freeze({
72
+ canMaterialize: canMaterializeRect,
73
+ op: "rematerializeRectMark",
74
+ positionEncoding: Object.freeze({ incomplete: "scale", scaleFirst: true }),
75
+ encoding: Object.freeze({ scaleFirst: true }),
76
+ scaleApplication: Object.freeze({
77
+ deferWithMark: true,
78
+ position: "rematerialize",
79
+ default: "defer"
80
+ }),
81
+ rematerializeIncompleteExisting: true
82
+ })
83
+ });
84
+
85
+ export function getMarkMaterializationPolicy(layer) {
86
+ return MARK_MATERIALIZATION_POLICIES[layer.mark?.type];
87
+ }
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  mapOrdinalPositionValues,
3
3
  readScaleField
4
- } from "../grammar/scales.js";
4
+ } from "../grammar/scales/index.js";
5
5
  import { RECT_MODES, resolveRectMode } from "../grammar/rects.js";
6
6
  import { DEFAULT_RECT_MARK } from "./rectConfig.js";
7
7
  import { mapScaleConsumerValues } from "./scales/map.js";
@@ -5,7 +5,7 @@ import {
5
5
  readQuantitativeField,
6
6
  readScaleField,
7
7
  readTemporalField
8
- } from "../grammar/scales.js";
8
+ } from "../grammar/scales/index.js";
9
9
  import { mapScaleConsumerValues } from "./scales/map.js";
10
10
 
11
11
  export function resolveRowEncodingValues(program, layer, dataset, channel) {
@@ -0,0 +1,94 @@
1
+ import { POSITION_ENCODING_CHANNELS } from "../core/vocabulary.js";
2
+ import { materializedLegendUsesScale } from "./legends.js";
3
+ import { buildMaterializationPlan } from "./planner.js";
4
+
5
+ const CARTESIAN_AXES = Object.freeze([
6
+ Object.freeze({ channel: "x", prefix: "X", lineObject: "xAxisLine" }),
7
+ Object.freeze({ channel: "y", prefix: "Y", lineObject: "yAxisLine" })
8
+ ]);
9
+ const POLAR_AXES = Object.freeze([
10
+ Object.freeze({ channel: "theta", prefix: "Theta" }),
11
+ Object.freeze({ channel: "radius", prefix: "Radial" })
12
+ ]);
13
+ const GRIDS = Object.freeze([
14
+ Object.freeze({ role: "horizontal", op: "rematerializeHorizontalGrid" }),
15
+ Object.freeze({ role: "vertical", op: "rematerializeVerticalGrid" }),
16
+ Object.freeze({ role: "theta", op: "rematerializeThetaGrid" }),
17
+ Object.freeze({ role: "radial", op: "rematerializeRadialGrid" })
18
+ ]);
19
+
20
+ function usesPositionalScale(program, id) {
21
+ return program.semanticSpec.layers.some(layer =>
22
+ POSITION_ENCODING_CHANNELS.some(
23
+ channel => layer.encoding?.[channel]?.scale === id
24
+ )
25
+ );
26
+ }
27
+
28
+ function usesRadialScale(program, id) {
29
+ return program.semanticSpec.layers.some(layer =>
30
+ layer.encoding?.radius?.scale === id
31
+ );
32
+ }
33
+
34
+ function semanticGuideUsesScale(program, id) {
35
+ return [
36
+ ...CARTESIAN_AXES.map(({ channel }) =>
37
+ program.semanticSpec.guides.axis?.[channel]
38
+ ),
39
+ ...POLAR_AXES.map(({ channel }) =>
40
+ program.semanticSpec.guides.axis?.[channel]
41
+ ),
42
+ ...GRIDS.map(({ role }) => program.semanticSpec.guides.grid?.[role])
43
+ ].some(guide => guide?.scale === id);
44
+ }
45
+
46
+ export function needsCanvasScaleRematerialization(program, scale) {
47
+ return (
48
+ (scale.range === "auto" ||
49
+ usesRadialScale(program, scale.id) ||
50
+ semanticGuideUsesScale(program, scale.id)) &&
51
+ program.resolvedScales[scale.id] !== undefined &&
52
+ usesPositionalScale(program, scale.id)
53
+ );
54
+ }
55
+
56
+ function componentEditOperation(prefix, component) {
57
+ const suffix = component[0].toUpperCase() + component.slice(1);
58
+ return `edit${prefix}Axis${suffix}`;
59
+ }
60
+
61
+ export function planScaleGuideRematerialization(program, id) {
62
+ const guides = [];
63
+ const objects = program.graphicSpec?.objects ?? {};
64
+ const guideConfigs = program.guideConfigs ?? {};
65
+ for (const axis of CARTESIAN_AXES) {
66
+ if (
67
+ objects[axis.lineObject] &&
68
+ program.semanticSpec.guides.axis?.[axis.channel]?.scale === id
69
+ ) {
70
+ guides.push({ op: componentEditOperation(axis.prefix, "line") });
71
+ }
72
+ for (const component of ["ticks", "labels", "title"]) {
73
+ if (guideConfigs.axis?.[axis.channel]?.[component]?.scale === id) {
74
+ guides.push({ op: componentEditOperation(axis.prefix, component) });
75
+ }
76
+ }
77
+ }
78
+ for (const axis of POLAR_AXES) {
79
+ for (const component of ["line", "ticks", "labels", "title"]) {
80
+ if (guideConfigs.axis?.[axis.channel]?.[component]?.scale === id) {
81
+ guides.push({ op: componentEditOperation(axis.prefix, component) });
82
+ }
83
+ }
84
+ }
85
+ for (const grid of GRIDS) {
86
+ if (guideConfigs.grid?.[grid.role]?.scale === id) {
87
+ guides.push({ op: grid.op });
88
+ }
89
+ }
90
+ if (materializedLegendUsesScale(program, id)) {
91
+ guides.push({ op: "rematerializeLegend" });
92
+ }
93
+ return buildMaterializationPlan({ guides });
94
+ }
@@ -6,7 +6,7 @@ import {
6
6
  mapDiscretizedColors,
7
7
  mapOrdinalValues,
8
8
  mapSequentialColors
9
- } from "../../grammar/scales.js";
9
+ } from "../../grammar/scales/index.js";
10
10
 
11
11
  export function mapScaleConsumerValues(values, resolvedScale, channel) {
12
12
  if (channel === "color" && isDiscretizedColorScaleType(resolvedScale.type)) {
@@ -2,7 +2,7 @@ import {
2
2
  isTransformedScaleType,
3
3
  resolveContinuousDomain,
4
4
  resolveTransformedDomain
5
- } from "../../../grammar/scales.js";
5
+ } from "../../../grammar/scales/index.js";
6
6
 
7
7
  export function resolveSeriesLayoutDomain({
8
8
  id,
@@ -24,7 +24,7 @@ import {
24
24
  validateOrdinalScaleType,
25
25
  validateScaleTypeForRole,
26
26
  validateTimeScaleType
27
- } from "../../grammar/scales.js";
27
+ } from "../../grammar/scales/index.js";
28
28
  import { resolveArcAutoPositionRange } from "./policies/arc.js";
29
29
  import {
30
30
  resolveBinnedBarDomain,
@@ -10,7 +10,7 @@ import { layoutSeriesPartition } from "../../../grammar/seriesLayout.js";
10
10
  import {
11
11
  readNominalField,
12
12
  readTemporalField
13
- } from "../../../grammar/scales.js";
13
+ } from "../../../grammar/scales/index.js";
14
14
  import { findSemanticScale } from "../../../selectors/scales.js";
15
15
  import { deriveHistogramSegments } from "../../bars/histogram.js";
16
16
  import {